← Back to Blog

What the Web Actually Runs On: jsDelivr Says a Korean Font, Not React

What the Web Actually Runs On: jsDelivr Says a Korean Font, Not React

I expected jsDelivr's bandwidth leaderboard to read like a roll call of the usual suspects: jQuery, React, Vue, Font Awesome, three.js, axios, lodash, core-js. I pulled the live stats API on 15 July 2026 for the complete month of June 2026, and not one of those is in the top 15 by bandwidth. Not for npm, not for GitHub.

The single largest bandwidth consumer on the entire network is a typeface. gh/orioncactus/pretendard, a Korean, Latin and Japanese webfont, served 3,099.55 TB in June 2026, which is 16.0% of all jsDelivr bandwidth for the month (stats API). React ranks #535 by bandwidth. core-js ranks #4,650.

That pair of facts is the whole article in miniature. A CDN byte ranking does not measure what the web is built on. It measures what gets shipped over that specific CDN as raw bytes. Anything that gets bundled by a build tool or self-hosted is nearly invisible, and anything big and binary dominates. Once you accept that, the leaderboard becomes genuinely useful, just not for the question you thought you were asking.

Method, so you can re-run it

All figures below come from the jsDelivr public stats API, fetched 15 July 2026, for the fixed period 2026-06. Bytes are converted with TB = 10^12 (decimal); the raw integers are what the API returns.

Three API details that will bite you. First, /v1/stats/packages defaults to sorting by hits, not bandwidth. The obvious-looking call ?period=month&type=npm returns swiper first, which is hits order. Pass &by=bandwidth explicitly or you silently get the wrong leaderboard. The OpenAPI spec is the authority on params; the API root itself returns HTTP 400, so do not cite it as docs.

Second, on the per-package endpoint, rank and typeRank are zero-indexed. Pretendard comes back as bandwidth.rank=0 while being first in the ordered list. Print that raw and you publish an off-by-one. Every rank here is already converted to 1-based.

Third, use the fixed period 2026-06, not period=month. month is a rolling window and it drifts: on my re-check, hls.js was 973.68 TB fixed and 1,012.05 TB rolling.

Table A: top 15 npm packages by bandwidth

Fetched 15 July 2026, period 2026-06. Source: stats API, npm by bandwidth.

# package TB (10^12) hits (B) avg/req what it is
1 hls.js 973.68 6.40 148.6 KB HLS video player
2 @lottiefiles/dotlottie-web 595.69 1.09 533.7 KB vector animation player
3 bootstrap 415.41 15.43 26.3 KB the CSS framework
4 swiper 375.91 18.91 19.4 KB touch slider, most hits of any npm package
5 pretendard 306.83 0.59 508.1 KB the webfont, npm copy
6 bootstrap-icons 250.18 4.74 51.6 KB SVG icon library
7 @mediapipe/tasks-vision 201.76 0.17 1141.3 KB on-device computer vision
8 mathjax 188.83 2.84 65.0 KB math typesetting
9 prebid.js 172.36 0.26 654.6 KB header bidding, ad tech
10 @mdi/font 172.10 1.35 124.9 KB Material Design icon font
11 yandex-metrica-watch 166.24 1.53 106.0 KB analytics shim
12 opencc-js 159.92 1.10 142.3 KB Chinese script conversion
13 monaco-editor 115.11 0.58 193.4 KB the VS Code editor core
14 onnxruntime-web 102.72 0.05 2069.5 KB ONNX models in the browser
15 video.js 96.83 1.18 80.4 KB HTML5 video player

Package identities are confirmed against the npm registry. Read that table again and notice what it is: video players, fonts, icon sets, ad tech, an analytics shim, a CJK text converter, and two AI runtimes. Bootstrap and Swiper are the only "classics" left standing.

Here is where the rest of them actually land for June 2026, by bandwidth, 1-based, overall then npm-only (per-package stats): jquery 58.05 TB (#53/#24), vue 54.52 TB (#56/#26), chart.js 53.29 TB (#59/#29), @fortawesome/fontawesome-free 48.74 TB (#64/#33), three 34.26 TB (#87/#46), axios 15.76 TB (#155/#85), lodash 6.90 TB (#291/#163), react 2.73 TB (#535/#293), core-js 0.04 TB (#4650/#2261).

React and core-js are two of the most depended-upon packages in the npm ecosystem and they are functionally absent. They get bundled. That is the cleanest available proof that bandwidth rank is not popularity.

Why the ranking looks like this

Bandwidth rank is approximately file size multiplied by popularity, and nothing else. Measured over the wire on 15 July 2026:

  • PretendardVariable.woff2 is 2,057,688 bytes per download. That one file served 469.75 TB in June 2026.
  • Pretendard-Regular.woff2 is 765,892 bytes and served 422.49 TB.
  • easylist.txt from uBlockOrigin/uAssetsCDN is 2,119,132 bytes and served 296.12 TB.
  • hls.min.js in hls.js@1.6.16, the version that drove 87.25% of that package's traffic, is 543,002 bytes and served 848.04 TB by itself.
  • bootstrap.min.css 5.3.8 is 232,111 bytes.

So Swiper has almost three times hls.js's hits (18.91B vs 6.40B) and ships 2.6x fewer bytes. If you print one column you mislead. Pretendard is a system-ui alternative blending Inter, Source Han Sans and the M+ Japanese face, released under the SIL Open Font License in nine weights plus a variable, and used by Naver Webtoon, Kakao Bank, Wanted, Musinsa, Class101, Inflearn and Adobe Fonts. Megabytes of font, at Korean-internet scale. That is the whole mechanism. (Counting all four Pretendard-related entries reaches roughly 20.6% of the network, but that aggregation is my own analytical choice, not a jsDelivr-published figure. The single defensible number is the 16.0% above.)

Table B: top 15 GitHub repos by bandwidth, and the part nobody advertises

Fetched 15 July 2026, period 2026-06. Repo existence re-checked via api.github.com the same day. Source: stats API, GitHub by bandwidth.

# repo TB hits (B) avg/req what it is / status
1 orioncactus/pretendard 3099.55 20.02 151.2 KB the webfont. 16.0% of the whole network
2 uBlockOrigin/uAssetsCDN 620.84 2.06 294.5 KB adblock filter lists pulled by browser extensions
3 adop-devel/jsdelivr 420.78 0.75 548.2 KB 0 stars. Re-hosts Pretendard .woff files
4 fonts-archive/AppleSDGothicNeo 203.69 0.19 1055.3 KB 4 stars. Korean font archive
5 Lexusbro/dummy 180.59 0.29 610.8 KB GitHub repo 404. One file, /dummy1.txt = 108.31 TB
6 fonts-archive/Pretendard 174.11 0.25 666.9 KB 16 stars. Another Pretendard mirror
7 msalvatorius/gladio 166.92 0.05 3097.2 KB 0 stars. Raw MP4 video
8 Project-Noonnu/noonfonts_2107 165.82 0.15 1113.2 KB 0 stars. Korean font collection
9 genizy/web-port 141.33 0.03 5237.2 KB 147 stars, "web port of pc games". Game .pck assets
10 hgkukigtfhnujh6-ops/extra 128.53 0.04 3162.6 KB 0 stars, no description. Randomly named hashed JS
11 hgkukigtfhnujh6-ops/temp 128.36 0.04 3136.0 KB same throwaway account as #10
12 shuvoads24-eng/azree2 96.01 0.03 3293.5 KB GitHub repo 404
13 shuvoads24-eng/azree1 95.98 0.03 3271.9 KB GitHub repo 404
14 ayanjuwel/love_my_bou 92.78 2.72 33.3 KB 0 stars. /live.json = 70.35 TB
15 ahsdev22-png/temp 89.98 0.03 3086.0 KB 0 stars. Randomly named hashed JS

jsDelivr's Terms of Use contain an Acceptable Use Policy prohibiting "using jsDelivr CDN as a general-purpose file or media hosting service", with the examples "running an image hosting website and using jsDelivr CDN as a storage for all uploaded images, hosting videos, file backups, or other files in large quantities."

Several rows above are exactly that. msalvatorius/gladio (166.92 TB, 3.1 MB average per request) serves /0416.mp4 at 77.36 TB and two more MP4s behind it. Lexusbro/dummy shipped 180.59 TB from a single text file. ayanjuwel/love_my_bou serves /live.json for 70.35 TB, a live sports-score feed polled as a free API backend. And a cluster of zero-star, no-description accounts named temp and extra is pushing randomly-named hashed JavaScript at about 3 MB a request. I did not download or analyse any of those payloads, so I am describing a pattern, not making a claim about what the code does. That is as far as the evidence goes.

Two rows deserve care. genizy/web-port is probably fine: the same AUP says "We recognize that there are legitimate projects that consist of a large number of files, and these are not considered abuse. For example: icons packs, apps, or games with a large number of assets." And uBlockOrigin/uAssetsCDN at 620.84 TB is not the web loading a library at all. It is millions of browser extensions fetching filter lists on a schedule.

The strongest fact in the table: three of the top 15 GitHub repos by bandwidth no longer exist on GitHub. Lexusbro/dummy, shuvoads24-eng/azree1 and azree2 all return 404 from api.github.com as of 15 July 2026, and the entire shuvoads24-eng account is gone too. That is 372.58 TB of June traffic attributable to repositories that have since vanished. Separately, June 2026 bandwidth fell 13.2% month over month (19.37 PB against 22.32 PB) while hits rose 1.7% (network stats). Bytes down, requests up. I found no jsDelivr announcement explaining it, so I am not going to claim a cause.

Table C: top 15 GitHub repos by hits, because ranking flips

Fetched 15 July 2026, period 2026-06. Source: stats API, GitHub by hits.

# repo hits (B) TB avg/req
1 prebid/currency-file 40.46 58.37 1.4 KB
2 orioncactus/pretendard 20.02 3099.55 151.2 KB
3 prebid/shared-id 10.86 8.29 0.7 KB
4 lipis/flag-icons 2.89 7.64 2.6 KB
5 ayanjuwel/love_my_bou 2.72 92.78 33.3 KB
6 foyer-work/cdn-files 2.35 1.45 0.6 KB
7 twitter/twemoji 2.33 3.21 1.3 KB
8 jdecked/twemoji 2.14 3.41 1.6 KB
9 uBlockOrigin/uAssetsCDN 2.06 620.84 294.5 KB
10 piedotorg/pie-adblock-rules 1.52 1.80 1.2 KB
11 spbgovbr-vlibras/vlibras-portal 1.46 81.55 54.4 KB
12 prebid/prebid-mobile-ios 1.20 14.17 11.6 KB
13 fancyapps/fancybox 0.85 13.31 15.3 KB
14 adop-devel/jsdelivr 0.75 420.78 548.2 KB
15 jimaek/testobjects 0.72 32.17 43.5 KB

The number one repository by request count is an ad-tech currency file, at 40.46 billion requests of 1.4 KB each. Prebid takes three of the top 12. Those are ad auctions, not developers. Fourth is a country-flag SVG set; eleventh is a Brazilian government sign-language accessibility portal. On the npm side by hits, swiper (18.91B) and bootstrap (15.43B) lead, and slick-carousel, a jQuery-era carousel, is still at 6.75B.

Network-wide for June 2026: 346,742,743,884 requests and 19,372,012,771,308,052 bytes, which is 19,372 TB, or 19.37 PB. Packages accounted for 98.08% of the bandwidth. Only two CDN providers reported traffic, Fastly at 55.2% of bytes and Cloudflare at 44.8%, though jsDelivr documents a multi-CDN arrangement using Cloudflare, Fastly and Gcore "to serve jsDelivr CDN traffic", with "#### Bunny (Limited to legacy bootstrapcdn.com CDN endpoints)".

What this changes about how you load things

A CDN URL is a supply-chain decision with a measurable cache consequence. Both halves are testable.

Pin the exact version. I measured the response headers on 15 July 2026:

URL shape Cache-Control returned
/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css public, max-age=31536000, s-maxage=31536000, immutable
/npm/bootstrap@5/... public, max-age=604800, s-maxage=43200
/npm/bootstrap@latest/... public, max-age=604800, s-maxage=43200
/npm/bootstrap/... public, max-age=604800, s-maxage=43200

A pinned version is cached for a year and marked immutable. A floating range gets seven days in the browser and twelve hours at the edge. Pinning is a security control that also happens to be a materially better cache.

SRI and floating ranges fail in the nastiest possible way. Right now /npm/bootstrap@5/dist/css/bootstrap.min.css is byte-identical to @5.3.8: both 232,111 bytes, both hashing to sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB, which reproduces Bootstrap's published integrity value exactly. So an SRI hash on a range works today, and then silently breaks the moment the range resolves to a new release, at which point the browser blocks the resource and your page breaks with no warning. Bootstrap's current documented version is 5.3.8, incidentally, not the 5.3.3 that AI assistants tend to emit from memory.

And SRI does not work on generated files at all, even pinned ones. jsDelivr's own +esm output carries the inline comment "Do NOT use SRI with dynamically generated files!" The reason is that "Minifying the exact same source code multiple times might not result in the exact same generated code", and "we might need to re-minify the file in certain situation, e.g., as a part of failover". Pinning fixes the version. It does not make a generated artifact byte-stable. So the "no build step" convenience and SRI are mutually exclusive. Pick knowingly.

Why any of this matters: polyfill.io. Cloudflare warned publicly on 29 February 2024 about "the transition of the domain polyfill.io to a new provider (Funnull)", noting that any site embedding the original domain "will now be relying on Funnull to maintain and secure the underlying project to avoid the risk of a supply chain attack." Sansec reported the malware on 25 June 2024: 100,000+ sites affected including JSTOR, Intuit and the World Economic Forum, with injected code that "redirects mobile users to a sports betting site using a fake Google analytics domain", activates only on specific mobile devices at specific hours, and stays dormant when it detects an admin user. Four months between the warning and the confirmed compromise. The domain changed hands; the script tags did not.

Building on it: the two-line version

The +esm path genuinely works. I downloaded https://cdn.jsdelivr.net/npm/nanoid@5.0.7/+esm and imported it in Node 24.15.0; it executed and exposed real named exports.

<script type="module">
  // Pin the exact version. No build step, no bundler.
  const { nanoid } = await import("https://cdn.jsdelivr.net/npm/nanoid@5.0.7/+esm");
  document.querySelector("#id").textContent = nanoid();

  // Lazy-load only on demand:
  document.querySelector("#celebrate").addEventListener("click", async () => {
    const confetti = (await import("https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm")).default;
    confetti();
  });
</script>

Two live-tested gotchas. The /combine endpoint requires full file paths: /combine/npm/canvas-confetti@1.9.3,npm/nanoid@5.0.7 returns 404, while /combine/npm/canvas-confetti@1.9.3/dist/confetti.browser.js,npm/nanoid@5.0.7/index.browser.js returns 200. And esm.run is jsDelivr's ESM service but it is still beta: "You can use esm.run links for quick demos or prototyping. In production (once this feature is out of beta), always use our main domain."

If you allow-list jsDelivr in CSP, know what you bought:

script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' https://cdn.jsdelivr.net;
font-src https://cdn.jsdelivr.net;
connect-src 'self' https://your-proxy.workers.dev;

CSP allow-lists the host, so that authorizes all of jsDelivr, not your specific file. That is precisely why SRI (byte-level) and CSP (host-level) are complementary rather than substitutes.

Building on it: the edge proxy, with the guardrails included

The moment you build a real tool on top of someone's API you want your own front door: caching, timeouts, a budget. Here is a Cloudflare Worker CORS proxy that does not become an open relay. It passes node --check and a 16-case test suite I ran locally, including a blocked open-relay attempt, a blocked subdomain-suffix bypass, a blocked cloud-metadata IP, a 429, and a 413 on an oversized body with no Content-Length.

const ALLOWED_ORIGINS = new Set(["https://yourtool.example", "http://localhost:8788"]);
const ALLOWED_TARGET_HOSTS = new Set(["api.example.com", "data.jsdelivr.com"]);
const TIMEOUT_MS = 8000;
const MAX_BYTES = 2 * 1024 * 1024;
const CACHE_TTL_SECONDS = 300;

function corsHeaders(origin) {
  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "GET, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Max-Age": "86400",
    Vary: "Origin",
  };
}
function deny(status, message, origin) {
  const headers = { "Content-Type": "application/json" };
  if (origin) Object.assign(headers, corsHeaders(origin));
  return new Response(JSON.stringify({ error: message }), { status, headers });
}
export default {
  async fetch(request, env, ctx) {
    const origin = request.headers.get("Origin") ?? "";
    const allowedOrigin = ALLOWED_ORIGINS.has(origin) ? origin : null;
    // Preflight is answered FIRST, or the browser blocks the real request.
    if (request.method === "OPTIONS") {
      if (!allowedOrigin) return new Response(null, { status: 403 });
      return new Response(null, { status: 204, headers: corsHeaders(allowedOrigin) });
    }
    if (!allowedOrigin) return deny(403, "origin_not_allowed", null);
    if (request.method !== "GET") return deny(405, "method_not_allowed", allowedOrigin);
    const target = new URL(request.url).searchParams.get("url");
    if (!target) return deny(400, "missing_url_param", allowedOrigin);
    let targetUrl;
    try { targetUrl = new URL(target); } catch { return deny(400, "invalid_url", allowedOrigin); }
    if (targetUrl.protocol !== "https:") return deny(400, "https_only", allowedOrigin);
    // Exact match. NEVER endsWith(): "api.example.com.attacker.test".endsWith("example.com") is true.
    if (!ALLOWED_TARGET_HOSTS.has(targetUrl.hostname)) return deny(403, "target_not_allowed", allowedOrigin);
    const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
    if (env.RATE_LIMITER) {
      const { success } = await env.RATE_LIMITER.limit({ key: ip });
      if (!success) return deny(429, "rate_limited", allowedOrigin);
    }
    const cacheKey = new Request(`https://proxy.internal/${encodeURIComponent(targetUrl.toString())}`, { method: "GET" });
    const cache = caches.default;
    const cached = await cache.match(cacheKey);
    if (cached) {
      const hit = new Response(cached.body, cached);
      Object.entries(corsHeaders(allowedOrigin)).forEach(([k, v]) => hit.headers.set(k, v));
      hit.headers.set("X-Proxy-Cache", "HIT");
      return hit;
    }
    let upstream;
    try {
      upstream = await fetch(targetUrl.toString(), {
        method: "GET",
        headers: { Accept: request.headers.get("Accept") ?? "*/*" },
        signal: AbortSignal.timeout(TIMEOUT_MS),
      });
    } catch { return deny(504, "upstream_timeout_or_error", allowedOrigin); }
    if (!upstream.ok) return deny(502, `upstream_status_${upstream.status}`, allowedOrigin);
    const declared = Number(upstream.headers.get("Content-Length") ?? "0");
    if (declared > MAX_BYTES) return deny(413, "response_too_large", allowedOrigin);
    const body = await limitBody(upstream.body, MAX_BYTES);
    if (body === null) return deny(413, "response_too_large", allowedOrigin);
    const response = new Response(body, {
      status: 200,
      headers: {
        "Content-Type": upstream.headers.get("Content-Type") ?? "application/octet-stream",
        "Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}`,
        ...corsHeaders(allowedOrigin),
        "X-Proxy-Cache": "MISS",
      },
    });
    ctx.waitUntil(cache.put(cacheKey, response.clone()));
    return response;
  },
};
async function limitBody(stream, maxBytes) {
  if (!stream) return new Uint8Array();
  const reader = stream.getReader();
  const chunks = [];
  let total = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    total += value.byteLength;
    if (total > maxBytes) { await reader.cancel(); return null; }
    chunks.push(value);
  }
  const out = new Uint8Array(total);
  let offset = 0;
  for (const c of chunks) { out.set(c, offset); offset += c.byteLength; }
  return out;
}

The rate limiter is a native binding, not a KV counter. Configure it in wrangler.jsonc, where the period must be either 10 or 60 seconds:

{
  "ratelimits": [
    { "name": "RATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 100, "period": 60 } }
  ]
}

Do not build that counter on Workers KV. As of 15 July 2026 the KV free tier allows 1,000 writes per day. One write per request exhausts your quota in minutes.

Be honest about what the limiter does, though: Cloudflare's own docs say rate limits "are local to the Cloudflare location that your Worker runs in" and that the API is "permissive, eventually consistent, and intentionally designed to not be used as an accurate accounting system." It is a cost-control speed bump, not a hard global cap. The docs also do not state whether the binding is available on the free plan, which is why the code guards with if (env.RATE_LIMITER) and degrades safely.

One more trap: cache theatre. The Cache API docs say "Workers deployed to custom domains have access to functional cache operations" and that dashboard-editor and Playground operations "will have no impact." Meanwhile Workers Caching is a separate, newer mechanism driven by ordinary Cache-Control headers that explicitly does run on workers.dev and collapses concurrent requests for the same key, which the Cache API does not.

The line that matters more than the code

The allow-list is not a disclaimer. It is the control.

An open CORS proxy, meaning Access-Control-Allow-Origin: * plus fetch(anything), will be found and used as an attack relay, and it will get your account terminated. The CORS header is not what prevents that; the target allow-list is. And it has to be exact-match membership, because "api.example.com.attacker.test".endsWith("example.com") evaluates to true. My test suite confirms the exact-match version returns 403 for that host. A suffix check silently reopens the relay you thought you closed.

The rest is the same ethic. Respect robots.txt, which is not folklore: it is RFC 9309, Standards Track, September 2022, and it specifies that crawlers "SHOULD NOT use the cached version for more than 24 hours." Respect the terms and the rate limits. jsDelivr's data API README says it plainly: "The API is free to use and imposes no rate limits. However, if you plan to make 100+ RPM for longer periods of time, you should contact us first." If a site blocks you, use its API, ask for access, or stop. There is nothing here about evading bot detection or defeating access controls, and the service being profiled draws the same line: its AUP bans "Trying to bypass our limits or restrictions in any way."

Where to run it, as of mid-2026

Both numbers below were fetched on 15 July 2026. Cloudflare's pricing docs were last updated 7 July 2026.

Cloudflare Workers Deno Deploy
Free requests 100,000/day, resets 00:00 UTC 1,000,000/month
Free compute 10 ms CPU per invocation 15 hours CPU/month + 350 GB-h memory
Free bandwidth not separately metered 20 GB egress/month
Free storage KV: 1 GB, 100k reads/day, 1,000 writes/day 1 GiB volume + 1 GiB KV
Other free limits 50 subrequests/request, 3 MB script, 128 MB memory 20 active deployments, 50 custom domains
Paid entry $5/month: 10M requests then $0.30/M, 30M CPU-ms then $0.02/M, no daily cap, 5 min CPU, 10,000 subrequests $20/month Pro: 5M requests ($2/M over), 200 GB ($0.50/GB over)

Two timing notes. Deno Deploy Classic shuts down on 20 July 2026, five days after I pulled these numbers. Nothing migrates automatically, KV databases require contacting support, and deployctl is retired in favour of the built-in deno deploy subcommand. And current Deno Deploy runs in two regions, US and Europe, down from six on Classic, which is a real Asia-Pacific latency regression. Re-check the pricing page before you commit; it is moving this week.

The takeaway

The leaderboard is worth reading precisely because it does not answer the question people ask of it. It tells you what is large, popular, unbundled, and served from this one CDN: fonts, video players, filter lists, ad-tech auction files, AI runtimes, and a quantity of raw MP4 that the platform's own terms prohibit. It is not a census of the web's dependencies, and any post that presents it as one has skipped the check that matters: React is #535.

So use it for what it is good for. Pin your versions, because the cache is better and the supply chain is safer. Put your own allow-listed, rate-limited, cached proxy in front of anything you fetch on a schedule. And when you go looking for "what the web runs on", go to a source that measures the thing you actually mean.

Related reading

Fact-check notes and sources


This post is informational, not engineering or security advice. Mentions of jsDelivr, Cloudflare, Deno, GitHub, and other third parties are nominative fair use, and no affiliation is implied. Rankings reflect a single fixed period (June 2026) fetched on 15 July 2026; pricing, free tiers, and platform limits are current as of mid-2026 and change quickly. Verify against the live sources before you build.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026