<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Afsar Khan</title>
    <description>The latest articles on DEV Community by Muhammad Afsar Khan (@afsar_khan).</description>
    <link>https://dev.to/afsar_khan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3773860%2F9b67157d-cdb3-432b-87b0-14a59e752dd0.jpg</url>
      <title>DEV Community: Muhammad Afsar Khan</title>
      <link>https://dev.to/afsar_khan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/afsar_khan"/>
    <language>en</language>
    <item>
      <title>Zero CLS, APCA Contrast, and AI Font Pairing: Building a Professional Font Comparison Tool.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Tue, 17 Mar 2026 12:46:43 +0000</pubDate>
      <link>https://dev.to/afsar_khan/zero-cls-apca-contrast-and-ai-font-pairing-building-a-professional-font-comparison-tool-6fp</link>
      <guid>https://dev.to/afsar_khan/zero-cls-apca-contrast-and-ai-font-pairing-building-a-professional-font-comparison-tool-6fp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8tnjdelubzknqx4007p.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8tnjdelubzknqx4007p.JPG" alt="Professional Font Comparison Tool"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes to web performance and accessibility, typography is often an afterthought. We pick nice fonts, maybe check contrast with a browser extension, and call it a day. But modern web development demands more.&lt;/p&gt;

&lt;p&gt;I recently built a professional font comparison tool that tackles three of the toughest challenges in web typography:&lt;/p&gt;

&lt;p&gt;1.&lt;strong&gt;Eliminating CLS&lt;/strong&gt; with font metric overrides&lt;/p&gt;

&lt;p&gt;2.&lt;strong&gt;Implementing APCA contras&lt;/strong&gt;t for WCAG 3 readiness&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI-powered font pairing&lt;/strong&gt; based on morphological characteristics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me walk you through the technical implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Zero CLS with Font Metric Overrides&lt;/strong&gt;&lt;br&gt;
Cumulative Layout Shift (CLS) occurs when a web font loads and swaps with the system fallback, changing the size of text elements. The fix? Use size-adjust, ascent-override, and descent-override in your @font-face rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge&lt;/strong&gt;: Every font requires different override values. Inter needs different adjustments than Roboto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Build a database of fallback metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_FALLBACKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-apple-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Segoe UI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;sizeAdjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;102.5%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ascentOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;92%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;descentOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;22%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lineGapOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Roboto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Segoe UI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;sizeAdjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;101.2%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ascentOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;94%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;descentOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;24%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lineGapOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 20+ more fonts with AI-matched metrics&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CLS Ghosting Visualization&lt;/strong&gt;: To help users understand the impact, I added a "ghost layer" that shows where the fallback font would render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showGhostLayers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Clone the preview text styles&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previewText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ghostDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ghostDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontWeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Apply fallback font&lt;/span&gt;
  &lt;span class="nx"&gt;ghostDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fallbackFont&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Calculate shift percentage&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sizeAdjust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`CLS Impact: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When activated, users see exactly how much the text would shift—visual feedback that drives home the importance of proper fallbacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Implementing APCA Contrast&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;WCAG 2.1&lt;/strong&gt; contrast ratios are simple but flawed. They don't account for font weight, spatial frequency, or perceptual brightness. APCA (Accessible Perceptual Contrast Algorithm) fixes this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The APCA Calculation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAPCAcontrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Convert to linear RGB&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sRGBtoY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rgbText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yBg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sRGBtoY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rgbBg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Determine which is lighter&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textIsLighter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yText&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;yBg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yHigh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yBg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yLow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yBg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// APCA formula (simplified)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textIsLighter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yHigh&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.56&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;yLow&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.57&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yHigh&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;yLow&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.62&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance Optimization&lt;/strong&gt;: APCA calculations can be expensive, especially when updating in real-time. I used a Web Worker to offload the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Worker thread&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;side&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateAPCA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;side&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Main thread&lt;/span&gt;
&lt;span class="nx"&gt;apcaWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updateWcagIndicatorsWithLC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Part 3: AI Morphology Pairing&lt;/strong&gt;&lt;br&gt;
Font pairing is part art, part science. I wanted to build an algorithm that could suggest pairings based on morphological characteristics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;smartMorphologyPair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourcePanel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourcePanel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFontCharacteristics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceFont&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Score potential matches&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFontCharacteristics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Contrasting families often work well&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;familyType&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;sourceChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;familyType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Matching x-heights creates harmony&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;xHeight&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sourceChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;xHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Similar style adds safety&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sourceChars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bestScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bestScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestScore&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This creates pairings like:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Playfair Display (serif) → Source Sans Pro (sans-serif) — 35 points for contrast&lt;/p&gt;

&lt;p&gt;Montserrat (geometric) → Open Sans (humanist) — complementary styles&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Lazy Loading 1000+ Fonts&lt;/strong&gt;&lt;br&gt;
Displaying every Google Font would crash the browser. The solution? Lazy loading with an Intersection Observer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupLazyLoadingForSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check if near bottom&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollPosition&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;scrollThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;loadMoreFonts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadMoreFonts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Show loading indicator&lt;/span&gt;
  &lt;span class="c1"&gt;// Fetch next batch of 50 fonts&lt;/span&gt;
  &lt;span class="c1"&gt;// Append to select options&lt;/span&gt;
  &lt;span class="c1"&gt;// Update lazy loading index&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Part 5: The Production Bundle Generator&lt;/strong&gt;&lt;br&gt;
The final piece: generating production-ready CSS that combines everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateProductionBundle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leftFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLeftFont&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rightFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRightFont&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leftFallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FALLBACKS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;leftFont&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rightFallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FALLBACKS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rightFont&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`
/* Google Fonts Import */
@import url('https://fonts.googleapis.com/css2?family=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFont&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;display=swap');

/* Zero-CLS Fallbacks */
@font-face {
  font-family: '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFont&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Fallback';
  src: local('&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;');
  size-adjust: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sizeAdjust&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
  ascent-override: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ascentOverride&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
  descent-override: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descentOverride&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
}

/* Font Stacks */
:root {
  --font-heading: '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFont&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFont&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Fallback', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leftFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;
  --font-body: '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rightFont&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rightFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;
}
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Result&lt;br&gt;
The tool now helps developers&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Eliminate CLS with one-click fallback generation&lt;/p&gt;

&lt;p&gt;Meet APCA contrast standards (future-proofing for WCAG 3)&lt;/p&gt;

&lt;p&gt;Find perfect font pairings using AI&lt;/p&gt;

&lt;p&gt;Test fonts in a live sandbox with real HTML/CSS&lt;/p&gt;

&lt;p&gt;If you're building a typography tool or just want to understand these concepts better, the full source approach is available in the tool. Check it out and let me know what you'd add!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the Pro Font Lab →&lt;/strong&gt; &lt;a href="https://fontpreview.online/comparison" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>a11y</category>
      <category>css</category>
    </item>
    <item>
      <title>Mastering Web Typography: Building a Real-Time Font Preview Tool with the Local Font Access API.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Fri, 13 Mar 2026 16:33:23 +0000</pubDate>
      <link>https://dev.to/afsar_khan/mastering-web-typography-building-a-real-time-font-preview-tool-with-the-local-font-access-api-4kfd</link>
      <guid>https://dev.to/afsar_khan/mastering-web-typography-building-a-real-time-font-preview-tool-with-the-local-font-access-api-4kfd</guid>
      <description>&lt;p&gt;We all know the struggle of choosing a font for a project. You have your design system in Figma, your VS Code open, and you're constantly switching back and forth to see if "Inter" really looks better than "Roboto" at 16px. Wouldn't it be great to just... preview them live, in the browser, alongside your actual content?&lt;/p&gt;

&lt;p&gt;In this post, I want to break down the core technical challenges I faced while building &lt;strong&gt;&lt;a href="https://fontpreview.online/preview" rel="noopener noreferrer"&gt;FontPreview.online&lt;/a&gt;&lt;/strong&gt; and share the solutions that made it fast, accurate, and developer-friendly.&lt;/p&gt;

&lt;p&gt;The Core Challenge: A Million Fonts, One Page&lt;br&gt;
The first hurdle was loading and displaying over 1,000 Google Fonts without crashing the browser. The naive approach-loading every single font stylesheet at once-is a performance nightmare.&lt;/p&gt;

&lt;p&gt;The Solution: Lazy Loading with an Intersection Observer.&lt;br&gt;
We load the base UI, but the actual font files are only fetched when a card is about to scroll into view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified lazy loading setup&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;loadGoogleFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Dynamically create &amp;lt;link&amp;gt; tag&lt;/span&gt;
      &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unobserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Start loading 100px before it enters&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.font-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "just-in-time" loading keeps the initial page weight low and the interaction smooth.&lt;/p&gt;

&lt;p&gt;Leveling Up with the Local Font Access API&lt;br&gt;
This was a game-changer. The window.queryLocalFonts() method allows us to ask a user for permission to read their locally installed fonts. No more guessing if "SF Mono" or "Helvetica Neue" is available!&lt;/p&gt;

&lt;p&gt;The Permission Flow is Critical.&lt;br&gt;
You can't just call the function. You need to handle the user's decision gracefully.&lt;/p&gt;

&lt;p&gt;Check Support: if ('queryLocalFonts' in window) { ... }&lt;/p&gt;

&lt;p&gt;Request and Handle Errors: The call prompts the user. We must handle the NotAllowedError if they deny it, and guide them on how to re-enable it if they change their mind.&lt;/p&gt;

&lt;p&gt;De-duplication: The API returns every font variation (e.g., "Arial Bold", "Arial Italic"). We had to map and clean these to present a simple list of unique font families.&lt;/p&gt;

&lt;p&gt;This feature is a huge win for developers who want to see exactly how their text will render on a target OS.&lt;/p&gt;

&lt;p&gt;Real-Time Accessibility (a11y) is Non-Negotiable&lt;br&gt;
A design tool is useless if it doesn't help you build for everyone. We integrated two levels of contrast checking:&lt;/p&gt;

&lt;p&gt;WCAG Ratio: The standard (L1 + 0.05) / (L2 + 0.05) formula.&lt;/p&gt;

&lt;p&gt;APCA (Accessible Perceptual Contrast Algorithm): This is the future (WCAG 3). It's more nuanced than the simple ratio, taking font weight and size into account. Implementing this was a deep dive into color science, but it provides a much more accurate prediction of readability.&lt;/p&gt;

&lt;p&gt;The Stack and Key Takeaways&lt;br&gt;
Vanilla JavaScript: Keeps it lightweight and fast for a core utility tool.&lt;/p&gt;

&lt;p&gt;CSS Grid: Made the responsive card layout trivial.&lt;/p&gt;

&lt;p&gt;LocalStorage: For persisting user preferences like pinned fonts and recent searches without a backend.&lt;/p&gt;

&lt;p&gt;Building a tool like this is a fantastic way to deepen your understanding of the modern web platform. You'll touch on performance APIs, cutting-edge browser features, and the nuanced world of accessibility.&lt;/p&gt;

&lt;p&gt;Check out the source code structure on the live site, and if you're working on a typography project, give the tool a spin!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>css</category>
      <category>a11y</category>
    </item>
    <item>
      <title>typography</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Fri, 27 Feb 2026 18:04:10 +0000</pubDate>
      <link>https://dev.to/afsar_khan/typography-1a3c</link>
      <guid>https://dev.to/afsar_khan/typography-1a3c</guid>
      <description></description>
    </item>
    <item>
      <title>How I Curated 100+ Google Font Pairings &amp; Built a Live Preview Tool....</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Mon, 23 Feb 2026 17:28:55 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-curated-100-google-font-pairings-built-a-live-preview-tool-4k3n</link>
      <guid>https://dev.to/afsar_khan/how-i-curated-100-google-font-pairings-built-a-live-preview-tool-4k3n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffefypr0notbz32epcuot.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffefypr0notbz32epcuot.JPG" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;strong&gt;Here's the thing: I'm terrible at picking fonts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time I started a project, I'd spend hours scrolling through Google Fonts, opening 20 tabs, trying to decide if Montserrat goes with Open Sans or if I should just give up and use Arial again.&lt;br&gt;
I knew I wasn't alone. Every designer I talked to had the same problem.&lt;br&gt;
So I built something simple: a page with 100+ font combinations that actually work, where you can type your own text and see it instantly.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem With Every Other Font Site
&lt;/h2&gt;

&lt;p&gt;Most font pairing websites show you pretty pictures of "Sample Text" with some made-up headline. But when I paste in my actual content — my product name, my blog title — it never looks as good.&lt;br&gt;
I wanted to see my words, in those fonts, right now.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I started by just listing out font pairs I actually use. Playfair Display + Lato for blog posts. Inter + Roboto for dashboards. The classics that never fail.&lt;br&gt;
Then I turned that list into JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
const pairings = [
  {
    heading: 'Playfair Display',
    body: 'Lato',
    style: 'classic',
    tags: ['serif', 'editorial'],
    description: 'Great for blogs and articles'
  },
  {
    heading: 'Inter',
    body: 'Roboto',
    style: 'modern',
    tags: ['ui', 'clean'],
    description: 'Perfect for web apps'
  }
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Magic Part: Live Preview
&lt;/h2&gt;

&lt;p&gt;The key feature is letting you type your own words. Here's how that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// When someone types in the box
customTextInput.addEventListener('input', (e) =&amp;gt; {
  customText = e.target.value || 'Visual Harmony';
  showAllPairings(); // Update everything
});

// Then each card shows their text
function showAllPairings() {
  pairings.forEach(pair =&amp;gt; {
    // Display their text in the heading font
    // Display description in the body font
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you type, all 100+ cards update. It's surprisingly fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making It Useful for Developers
&lt;/h2&gt;

&lt;p&gt;The thing I wanted most was to copy-paste CSS quickly. So I added a button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function copyCSS(heading, body) {
  const css = `/* Use these in your project */
h1, h2, h3 {
  font-family: '${heading}';
}

p {
  font-family: '${body}';
}`;

  navigator.clipboard.writeText(css);
  alert('Copied! Ready to paste into your CSS');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One click, and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping It Fast
&lt;/h2&gt;

&lt;p&gt;With 100+ cards, the page could get slow. So I only load 12 at first, then add more when you click "Load More":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let visibleCount = 12;

function loadMore() {
  visibleCount = visibleCount + 12;
  showPairings(); // Show the next batch
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple but works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;People actually use the custom text box — like, a lot. Everyone wants to see their own words.&lt;/li&gt;
&lt;li&gt;Categories help more than search — "Show me modern fonts" is easier than typing keywords.&lt;/li&gt;
&lt;li&gt;Copy buttons get clicked — developers hate typing font names manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The whole thing is free, no signup needed: &lt;a href="https://fontpreview.online/pairings" rel="noopener noreferrer"&gt;fontpreview.online/pairings&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;You can:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test 100+ combinations with your text&lt;/li&gt;
&lt;li&gt;Filter by style (modern, classic, elegant)&lt;/li&gt;
&lt;li&gt;Copy CSS instantly&lt;/li&gt;
&lt;li&gt;Preview on desktop and mobile&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variable font pairings (experiment with weight axes)&lt;/li&gt;
&lt;li&gt;Dark mode preview (see how pairs look on dark backgrounds)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - Export as PDF (for client presentations)
&lt;/h2&gt;

&lt;p&gt;Built with vanilla JS, Google Fonts API, and a lot of caffeine. Free forever, no signup required.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
      <category>design</category>
    </item>
    <item>
      <title>How I Built a Side-by-Side Font Comparison Tool (And Accidentally Learned Way Too Much About Browser APIs)</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Thu, 19 Feb 2026 09:24:29 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-built-a-side-by-side-font-comparison-tool-and-accidentally-learned-way-too-much-about-1l12</link>
      <guid>https://dev.to/afsar_khan/how-i-built-a-side-by-side-font-comparison-tool-and-accidentally-learned-way-too-much-about-1l12</guid>
      <description>&lt;p&gt;&lt;strong&gt;I wanted&lt;/strong&gt; to let people compare system fonts with Google Fonts.&lt;/p&gt;

&lt;p&gt;Sounds simple, right?&lt;/p&gt;

&lt;p&gt;It was not simple.&lt;/p&gt;

&lt;p&gt;But after a lot of trial, error, and yelling at my browser console, I got it working. Here's how it works — including the parts that almost made me give up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt; because I was tired of guessing how fonts would look with real text. But the thing designers kept asking was: "Can I compare this Google Font with the font already on my computer?"&lt;/p&gt;

&lt;p&gt;Turns out, that's harder than it sounds.&lt;/p&gt;

&lt;p&gt;Browsers don't really want you poking around someone's system fonts. For good reason — imagine every website you visit getting a list of everything installed on your computer. That's a privacy nightmare.&lt;/p&gt;

&lt;p&gt;But there's a newer API that lets you do this, if the user says it's okay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Local Font Access API
&lt;/h2&gt;

&lt;p&gt;There's this thing called the Local Font Access API. It's relatively new, and not every browser supports it yet (looking at you, Safari). But in Chrome and Edge, you can do this: &lt;strong&gt;javascript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const fonts = await window.queryLocalFonts();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line of code returns an array of every font installed on the user's system.&lt;/p&gt;

&lt;p&gt;Except — and this is important — the browser will ask the user for permission first. A little popup shows up saying "This site wants to see your fonts." If the user says no, you get nothing.&lt;/p&gt;

&lt;p&gt;This is good. We don't want random sites scraping font lists without permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Permission Dance
&lt;/h2&gt;

&lt;p&gt;Here's how I handle it: &lt;strong&gt;javascript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function checkSystemFontsPermission() {
  if (!window.queryLocalFonts) {
    showToast('Local Font Access API not supported in this browser');
    useFallbackFonts();
    return;
  }

  // Show the permission modal
  document.getElementById('permissionModal').style.display = 'flex';
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the browser doesn't support the API, I fall back to a list of popular system fonts. Not perfect, but better than nothing.&lt;/p&gt;

&lt;p&gt;If it does support it, I show a little modal explaining why I'm asking and what I'll do with the data. (Spoiler: nothing. I just show them in a dropdown.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When They Say Yes
&lt;/h2&gt;

&lt;p&gt;When the user clicks "Allow", this runs: &lt;strong&gt;javascript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function requestFontPermission() {
  try {
    const fonts = await window.queryLocalFonts();

    // Clean up font names (remove "Regular", "Bold", etc.)
    const fontMap = new Map();

    fonts.forEach(f =&amp;gt; {
      let name = f.family;
      const suffixes = [' Regular', ' Bold', ' Italic', ' Light', ' Medium'];

      suffixes.forEach(suffix =&amp;gt; {
        if (name.endsWith(suffix)) {
          name = name.substring(0, name.length - suffix.length);
        }
      });

      if (!fontMap.has(name)) {
        fontMap.set(name, {
          family: name,
          fullName: f.family,
          style: f.style,
          weight: f.weight
        });
      }
    });

    // Sort and store
    allSystemFonts = Array.from(fontMap.values()).sort((a, b) =&amp;gt; 
      a.family.localeCompare(b.family)
    );

    showToast(`Loaded ${allSystemFonts.length} system fonts`);

  } catch (error) {
    if (error.name === 'NotAllowedError') {
      showToast('Permission denied. Using fallback fonts.');
    } else {
      showToast('Error loading fonts. Using fallback.');
    }
    useFallbackFonts();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Map thing is important. A lot of fonts come back with multiple entries for different styles — "Arial Regular", "Arial Bold", "Arial Italic". I just want "Arial" once. The Map deduplicates them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Problem I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;Once I had the font list, I needed to actually use those fonts in the preview.&lt;/p&gt;

&lt;p&gt;In CSS, you can just do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;font-family: 'Arial', sans-serif;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if the user has Arial installed, it works. Great.&lt;/p&gt;

&lt;p&gt;But here's the thing — I also wanted to let users compare system fonts with Google Fonts side by side. So if someone picks Arial on the left and Roboto on the right, I need to load Roboto from Google Fonts.&lt;/p&gt;

&lt;p&gt;That means dynamically injecting a  tag: &lt;strong&gt;javascript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function loadGoogleFontForPanel(fontName, panel) {
  const fontFamily = fontName.replace(/ /g, '+');

  const linkId = `panel-font-${panel}`;
  const oldLink = document.getElementById(linkId);
  if (oldLink) oldLink.remove();

  const link = document.createElement('link');
  link.id = linkId;
  link.rel = 'stylesheet';
  link.href = `https://fonts.googleapis.com/css2?family=${fontFamily}&amp;amp;display=swap`;

  document.head.appendChild(link);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, each panel can load its own font independently. Left panel can load a system font (which doesn't need a stylesheet), right panel can load a Google Font (which does).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Still Bugs Me
&lt;/h2&gt;

&lt;p&gt;When you load a Google Font dynamically, there's a tiny delay before it's available. During that moment, the text shows up in the default font, then swaps to the chosen font.&lt;/p&gt;

&lt;p&gt;It's a flash of unstyled text. FOIT, if you want the fancy term.&lt;/p&gt;

&lt;p&gt;I tried a bunch of fixes. Preloading, font-display: swap, even hiding the text until the font loads. Everything felt janky.&lt;/p&gt;

&lt;p&gt;Eventually I just left it. The flash is brief, and most users don't notice. But I notice. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I built this again from scratch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache the permission status.&lt;/strong&gt; Right now, the modal shows every time you click "System". That's annoying. I should store their choice in localStorage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better error handling.&lt;/strong&gt; Sometimes the API just... fails. No error, no nothing. The code just stops. I need to catch that better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback for Safari.&lt;/strong&gt; Safari doesn't support this API at all. I should build a real fallback instead of just a list of popular fonts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code (If You Want It)
&lt;/h2&gt;

&lt;p&gt;The whole thing is on &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt; if you want to see it in action. View source — it's all client-side, no backend, no tracking, just HTML, CSS, and JavaScript.&lt;/p&gt;

&lt;p&gt;I'm not a great developer. I learned most of this by breaking things and Googling errors. But it works, and people use it, and that's enough for me.&lt;/p&gt;

&lt;p&gt;If you're building something with the Local Font Access API, hit me up. I've probably already hit the same bugs you're hitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Originally published on &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt;. Try the tool here: &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Tags: javascript, webdev, tutorial, showdev, api&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a WCAG Contrast Checker in 50 Lines of JavaScript.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Thu, 19 Feb 2026 08:56:21 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-28f7</link>
      <guid>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-28f7</guid>
      <description>&lt;p&gt;I used to do something really dumb.&lt;/p&gt;

&lt;p&gt;When I was picking colors for a website, I'd zoom in, squint, and ask myself: "Is this readable? Yeah, probably."&lt;/p&gt;

&lt;p&gt;Then I'd ship it.&lt;/p&gt;

&lt;p&gt;A few months ago, a user emailed me. He had low vision and couldn't read light gray text on one of my sites. He wasn't angry — just disappointed. Said he wanted to read my content but couldn't.&lt;/p&gt;

&lt;p&gt;That email is still in my inbox.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;, I decided I wasn't going to guess anymore. I built a contrast checker right into the tool. Turns out it only took about 50 lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's exactly how it works.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Math (It's Not as Scary as It Looks)&lt;/strong&gt;&lt;br&gt;
WCAG contrast ratio is calculated with this formula:&lt;br&gt;
&lt;code&gt;(L1 + 0.05) / (L2 + 0.05)&lt;/code&gt;&lt;br&gt;
Where L1 is the relative luminance of the lighter color, and L2 is the relative luminance of the darker color.&lt;/p&gt;

&lt;p&gt;But "relative luminance" sounds like something from a math textbook. Here's what it actually looks like in JavaScript:&lt;br&gt;
&lt;code&gt;function luminance(r, g, b) {&lt;br&gt;
  let a = [r, g, b].map(v =&amp;gt; {&lt;br&gt;
    v /= 255;&lt;br&gt;
    return v &amp;lt;= 0.03928 &lt;br&gt;
      ? v / 12.92 &lt;br&gt;
      : Math.pow((v + 0.055) / 1.055, 2.4);&lt;br&gt;
  });&lt;br&gt;
  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  I didn't invent this formula — it's straight from the WCAG spec. I just typed it into my editor and hoped it worked. (It did. Eventually.)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Converting Hex to RGB&lt;/strong&gt;&lt;br&gt;
First step: turn those hex codes (#1e1e1e, #E8E8E8) into something we can do math with.&lt;br&gt;
[function hexToRgb(hex) {&lt;br&gt;
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);&lt;br&gt;
  return result ? {&lt;br&gt;
    r: parseInt(result[1], 16),&lt;br&gt;
    g: parseInt(result[2], 16),&lt;br&gt;
    b: parseInt(result3], 16)&lt;br&gt;
  } : null;&lt;br&gt;
}&lt;/p&gt;
&lt;h2&gt;
  
  
  Regex looks scary, but it's just pulling out the red, green, and blue values from that hex string.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Putting It All Together&lt;/strong&gt;&lt;br&gt;
Once we have RGB values, we can calculate the contrast ratio between two colors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function contrastRatio(hex1, hex2) {
  const rgb1 = hexToRgb(hex1);
  const rgb2 = hexToRgb(hex2);

  if (!rgb1 || !rgb2) return 1;

  const l1 = luminance(rgb1.r, rgb1.g, rgb1.b);
  const l2 = luminance(rgb2.r, rgb2.g, rgb2.b);

  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);

  return (lighter + 0.05) / (darker + 0.05);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  That's it. That's the whole thing.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's a "Good" Ratio?&lt;/strong&gt;&lt;br&gt;
WCAG defines three levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAA&lt;/strong&gt;: 7:1 or higher (the gold standard)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA&lt;/strong&gt;: 4.5:1 or higher (what most sites should aim for)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA Large&lt;/strong&gt;: 3:1 for text that's at least 24px or 19px bold&lt;/p&gt;

&lt;p&gt;Here's how I check that in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getWcagLevel(ratio, fontSize, isBold) {
  const size = parseFloat(fontSize);
  const isLarge = size &amp;gt;= 18.66 || (size &amp;gt;= 14 &amp;amp;&amp;amp; isBold);

  if (ratio &amp;gt;= 7) return { level: 'AAA', class: 'aaa' };
  if (ratio &amp;gt;= 4.5) return { level: 'AA', class: 'pass' };
  if (ratio &amp;gt;= 3 &amp;amp;&amp;amp; isLarge) return { level: 'AA Large', class: 'aa-large' };
  return { level: 'FAIL', class: 'fail' };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 18.66 and 14 numbers come from the WCAG spec — 18.66px is about 14pt at 1.333x scaling. I don't think about it too hard. I just copy the numbers and move on.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Part That Surprised Me&lt;/strong&gt;&lt;br&gt;
I thought the math would be the hard part. It wasn't.&lt;/p&gt;

&lt;p&gt;The hard part was realizing how many of my old designs would have failed these checks. That light gray text I thought looked "clean" and "minimal"? FAIL. That low-contrast button I was proud of? FAIL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email from that user makes more sense now.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You Can Try It Yourself&lt;/strong&gt;&lt;br&gt;
I built this into &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;. Pick any two colors, type some text, and the badge updates instantly. No math required.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're a developer, steal this code. Use it in your own projects. That's why I'm sharing it.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One More Thing&lt;/strong&gt;&lt;br&gt;
The 50 lines of code in this post are fine. They work. But they're not the important part.&lt;/p&gt;

&lt;p&gt;The important part is that I stopped guessing.&lt;/p&gt;

&lt;p&gt;If you're still checking colors by squinting at your screen, just know that someone out there — maybe someone like the guy who emailed me — is trying to read your work and can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty lines of code is a small price to pay for making sure they can.
&lt;/h2&gt;

&lt;p&gt;Try the contrast checker yourself: &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built a WCAG Contrast Checker in 50 Lines of JavaScript.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Wed, 18 Feb 2026 11:38:33 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-1lo5</link>
      <guid>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-1lo5</guid>
      <description>&lt;p&gt;I used to do something really dumb.&lt;/p&gt;

&lt;p&gt;When I was picking colors for a website, I'd zoom in, squint, and ask myself: "Is this readable? Yeah, probably."&lt;/p&gt;

&lt;p&gt;Then I'd ship it.&lt;/p&gt;

&lt;p&gt;A few months ago, a user emailed me. He had low vision and couldn't read light gray text on one of my sites. He wasn't angry — just disappointed. Said he wanted to read my content but couldn't.&lt;/p&gt;

&lt;p&gt;That email is still in my inbox.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;, I decided I wasn't going to guess anymore. I built a contrast checker right into the tool. Turns out it only took about 50 lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's exactly how it works.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Math (It's Not as Scary as It Looks)&lt;/strong&gt;&lt;br&gt;
WCAG contrast ratio is calculated with this formula:&lt;br&gt;
&lt;code&gt;(L1 + 0.05) / (L2 + 0.05)&lt;/code&gt;&lt;br&gt;
Where L1 is the relative luminance of the lighter color, and L2 is the relative luminance of the darker color.&lt;/p&gt;

&lt;p&gt;But "relative luminance" sounds like something from a math textbook. Here's what it actually looks like in JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function luminance(r, g, b) {
  let a = [r, g, b].map(v =&amp;gt; {
    v /= 255;
    return v &amp;lt;= 0.03928 
      ? v / 12.92 
      : Math.pow((v + 0.055) / 1.055, 2.4);
  });
  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  I didn't invent this formula — it's straight from the WCAG spec. I just typed it into my editor and hoped it worked. (It did. Eventually.)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Converting Hex to RGB&lt;/strong&gt;&lt;br&gt;
First step: turn those hex codes (#1e1e1e, #E8E8E8) into something we can do math with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Regex looks scary, but it's just pulling out the red, green, and blue values from that hex string.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Putting It All Together&lt;/strong&gt;&lt;br&gt;
Once we have RGB values, we can calculate the contrast ratio between two colors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function contrastRatio(hex1, hex2) {
  const rgb1 = hexToRgb(hex1);
  const rgb2 = hexToRgb(hex2);

  if (!rgb1 || !rgb2) return 1;

  const l1 = luminance(rgb1.r, rgb1.g, rgb1.b);
  const l2 = luminance(rgb2.r, rgb2.g, rgb2.b);

  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);

  return (lighter + 0.05) / (darker + 0.05);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  That's it. That's the whole thing.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's a "Good" Ratio?&lt;/strong&gt;&lt;br&gt;
WCAG defines three levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAA&lt;/strong&gt;: 7:1 or higher (the gold standard)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA&lt;/strong&gt;: 4.5:1 or higher (what most sites should aim for)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA Large&lt;/strong&gt;: 3:1 for text that's at least 24px or 19px bold&lt;/p&gt;

&lt;p&gt;Here's how I check that in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function getWcagLevel(ratio, fontSize, isBold) {
  const size = parseFloat(fontSize);
  const isLarge = size &amp;gt;= 18.66 || (size &amp;gt;= 14 &amp;amp;&amp;amp; isBold);

  if (ratio &amp;gt;= 7) return { level: 'AAA', class: 'aaa' };
  if (ratio &amp;gt;= 4.5) return { level: 'AA', class: 'pass' };
  if (ratio &amp;gt;= 3 &amp;amp;&amp;amp; isLarge) return { level: 'AA Large', class: 'aa-large' };
  return { level: 'FAIL', class: 'fail' };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 18.66 and 14 numbers come from the WCAG spec — 18.66px is about 14pt at 1.333x scaling. I don't think about it too hard. I just copy the numbers and move on.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Part That Surprised Me&lt;/strong&gt;&lt;br&gt;
I thought the math would be the hard part. It wasn't.&lt;/p&gt;

&lt;p&gt;The hard part was realizing how many of my old designs would have failed these checks. That light gray text I thought looked "clean" and "minimal"? FAIL. That low-contrast button I was proud of? FAIL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email from that user makes more sense now.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You Can Try It Yourself&lt;/strong&gt;&lt;br&gt;
I built this into &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;. Pick any two colors, type some text, and the badge updates instantly. No math required.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're a developer, steal this code. Use it in your own projects. That's why I'm sharing it.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One More Thing&lt;/strong&gt;&lt;br&gt;
The 50 lines of code in this post are fine. They work. But they're not the important part.&lt;/p&gt;

&lt;p&gt;The important part is that I stopped guessing.&lt;/p&gt;

&lt;p&gt;If you're still checking colors by squinting at your screen, just know that someone out there — maybe someone like the guy who emailed me — is trying to read your work and can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty lines of code is a small price to pay for making sure they can.
&lt;/h2&gt;

&lt;p&gt;Try the contrast checker yourself: &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>wcag</category>
      <category>css</category>
    </item>
    <item>
      <title>How I Built a WCAG Contrast Checker in 50 Lines of JavaScript.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Wed, 18 Feb 2026 11:38:33 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-3nbd</link>
      <guid>https://dev.to/afsar_khan/how-i-built-a-wcag-contrast-checker-in-50-lines-of-javascript-3nbd</guid>
      <description>&lt;p&gt;I used to do something really dumb.&lt;/p&gt;

&lt;p&gt;When I was picking colors for a website, I'd zoom in, squint, and ask myself: "Is this readable? Yeah, probably."&lt;/p&gt;

&lt;p&gt;Then I'd ship it.&lt;/p&gt;

&lt;p&gt;A few months ago, a user emailed me. He had low vision and couldn't read light gray text on one of my sites. He wasn't angry — just disappointed. Said he wanted to read my content but couldn't.&lt;/p&gt;

&lt;p&gt;That email is still in my inbox.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;, I decided I wasn't going to guess anymore. I built a contrast checker right into the tool. Turns out it only took about 50 lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's exactly how it works.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Math (It's Not as Scary as It Looks)&lt;/strong&gt;&lt;br&gt;
WCAG contrast ratio is calculated with this formula:&lt;br&gt;
&lt;code&gt;(L1 + 0.05) / (L2 + 0.05)&lt;/code&gt;&lt;br&gt;
Where L1 is the relative luminance of the lighter color, and L2 is the relative luminance of the darker color.&lt;/p&gt;

&lt;p&gt;But "relative luminance" sounds like something from a math textbook. Here's what it actually looks like in JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function luminance(r, g, b) {
  let a = [r, g, b].map(v =&amp;gt; {
    v /= 255;
    return v &amp;lt;= 0.03928 
      ? v / 12.92 
      : Math.pow((v + 0.055) / 1.055, 2.4);
  });
  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  I didn't invent this formula — it's straight from the WCAG spec. I just typed it into my editor and hoped it worked. (It did. Eventually.)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Converting Hex to RGB&lt;/strong&gt;&lt;br&gt;
First step: turn those hex codes (#1e1e1e, #E8E8E8) into something we can do math with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Regex looks scary, but it's just pulling out the red, green, and blue values from that hex string.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Putting It All Together&lt;/strong&gt;&lt;br&gt;
Once we have RGB values, we can calculate the contrast ratio between two colors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function contrastRatio(hex1, hex2) {
  const rgb1 = hexToRgb(hex1);
  const rgb2 = hexToRgb(hex2);

  if (!rgb1 || !rgb2) return 1;

  const l1 = luminance(rgb1.r, rgb1.g, rgb1.b);
  const l2 = luminance(rgb2.r, rgb2.g, rgb2.b);

  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);

  return (lighter + 0.05) / (darker + 0.05);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  That's it. That's the whole thing.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's a "Good" Ratio?&lt;/strong&gt;&lt;br&gt;
WCAG defines three levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAA&lt;/strong&gt;: 7:1 or higher (the gold standard)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA&lt;/strong&gt;: 4.5:1 or higher (what most sites should aim for)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AA Large&lt;/strong&gt;: 3:1 for text that's at least 24px or 19px bold&lt;/p&gt;

&lt;p&gt;Here's how I check that in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javascript
function getWcagLevel(ratio, fontSize, isBold) {
  const size = parseFloat(fontSize);
  const isLarge = size &amp;gt;= 18.66 || (size &amp;gt;= 14 &amp;amp;&amp;amp; isBold);

  if (ratio &amp;gt;= 7) return { level: 'AAA', class: 'aaa' };
  if (ratio &amp;gt;= 4.5) return { level: 'AA', class: 'pass' };
  if (ratio &amp;gt;= 3 &amp;amp;&amp;amp; isLarge) return { level: 'AA Large', class: 'aa-large' };
  return { level: 'FAIL', class: 'fail' };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 18.66 and 14 numbers come from the WCAG spec — 18.66px is about 14pt at 1.333x scaling. I don't think about it too hard. I just copy the numbers and move on.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Part That Surprised Me&lt;/strong&gt;&lt;br&gt;
I thought the math would be the hard part. It wasn't.&lt;/p&gt;

&lt;p&gt;The hard part was realizing how many of my old designs would have failed these checks. That light gray text I thought looked "clean" and "minimal"? FAIL. That low-contrast button I was proud of? FAIL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email from that user makes more sense now.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You Can Try It Yourself&lt;/strong&gt;&lt;br&gt;
I built this into &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;. Pick any two colors, type some text, and the badge updates instantly. No math required.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're a developer, steal this code. Use it in your own projects. That's why I'm sharing it.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One More Thing&lt;/strong&gt;&lt;br&gt;
The 50 lines of code in this post are fine. They work. But they're not the important part.&lt;/p&gt;

&lt;p&gt;The important part is that I stopped guessing.&lt;/p&gt;

&lt;p&gt;If you're still checking colors by squinting at your screen, just know that someone out there — maybe someone like the guy who emailed me — is trying to read your work and can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty lines of code is a small price to pay for making sure they can.
&lt;/h2&gt;

&lt;p&gt;Try the contrast checker yourself: &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>wcag</category>
      <category>css</category>
    </item>
    <item>
      <title>How I Reduced Font Load Time by 70% Using Variable Fonts.</title>
      <dc:creator>Muhammad Afsar Khan</dc:creator>
      <pubDate>Sun, 15 Feb 2026 10:55:27 +0000</pubDate>
      <link>https://dev.to/afsar_khan/how-i-reduced-font-load-time-by-70-using-variable-fonts-3pnm</link>
      <guid>https://dev.to/afsar_khan/how-i-reduced-font-load-time-by-70-using-variable-fonts-3pnm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I used to think typography was just about aesthetics. Pick a nice font, pair it with another nice font, ship it.&lt;/p&gt;

&lt;p&gt;Then I launched a client site that looked &lt;em&gt;perfect&lt;/em&gt; on my fiber-optic connection — but on a real 4G network, the text area stayed blank for 2.7 seconds. The client's bounce rate doubled.&lt;/p&gt;

&lt;p&gt;That's when I stopped treating fonts as "design assets" and started treating them as what they really are: &lt;strong&gt;render-blocking resources&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: 6 Font Files, 380kb
&lt;/h2&gt;

&lt;p&gt;Here's what I did for years: when I needed a font for a project, I'd load the entire family. Roboto? Give me Thin, Light, Regular, Medium, Bold, Black. "Just in case."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; 380kb of font files for text that only ever used Regular and Bold.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: One Variable Font, 112kb
&lt;/h2&gt;

&lt;p&gt;Variable fonts changed everything. One file contains &lt;em&gt;all&lt;/em&gt; the variations — weight from 100 to 900, width from condensed to extended, slant from upright to italic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The math:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6 static files → &lt;strong&gt;380kb&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;1 variable file → &lt;strong&gt;112kb&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;70% reduction&lt;/strong&gt; in font payload&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Results (Real Numbers)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blog&lt;/td&gt;
&lt;td&gt;412kb&lt;/td&gt;
&lt;td&gt;118kb&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corporate site&lt;/td&gt;
&lt;td&gt;384kb&lt;/td&gt;
&lt;td&gt;112kb&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce&lt;/td&gt;
&lt;td&gt;520kb&lt;/td&gt;
&lt;td&gt;156kb&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average First Contentful Paint improvement: &lt;strong&gt;0.8 seconds faster on 4G&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When NOT to Use Variable Fonts
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody tells you: &lt;strong&gt;variable fonts aren't always better&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you only need 1-2 weights, static fonts might be smaller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 static weights:&lt;/strong&gt; ~84kb&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 variable font:&lt;/strong&gt; ~112kb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use variable fonts when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need 3+ weights&lt;/li&gt;
&lt;li&gt;You want fine control (weight 450, 560, etc.)&lt;/li&gt;
&lt;li&gt;You need multiple axes (width, slant)&lt;/li&gt;
&lt;li&gt;Performance is critical (one request vs multiple)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Go-To Variable Fonts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Font&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;112kb&lt;/td&gt;
&lt;td&gt;Most projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roboto Flex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;487kb&lt;/td&gt;
&lt;td&gt;When you need everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merriweather Sans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;98kb&lt;/td&gt;
&lt;td&gt;Content sites, blogs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fraunces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;124kb&lt;/td&gt;
&lt;td&gt;Brands with personality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Performance Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Count your weights – Do you really need all 9?&lt;/li&gt;
&lt;li&gt;[ ] Check variable file size – Compare with static alternatives&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;font-display: swap&lt;/code&gt; – Never hide text&lt;/li&gt;
&lt;li&gt;[ ] Preload one font – Only the one above the fold&lt;/li&gt;
&lt;li&gt;[ ] Test on 3G – If it loads in 2 seconds, you're good&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I still don't use variable fonts for every project. Sometimes two static weights really are the right answer. But now I check. I open &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt;, see the file size, count the weights I actually need, and make a real decision instead of just guessing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Try it yourself: &lt;a href="https://fontpreview.online" rel="noopener noreferrer"&gt;FontPreview&lt;/a&gt; – free tool to test variable fonts with your own text&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>performance</category>
      <category>typography</category>
    </item>
  </channel>
</rss>
