<?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: Vadym Liubyvyi</title>
    <description>The latest articles on DEV Community by Vadym Liubyvyi (@liubyvyi).</description>
    <link>https://dev.to/liubyvyi</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4202007%2F0f0e4bae-5190-49bb-990f-be5f76d7db1b.png</url>
      <title>DEV Community: Vadym Liubyvyi</title>
      <link>https://dev.to/liubyvyi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/liubyvyi"/>
    <language>en</language>
    <item>
      <title>Accessibility testing with Playwright and axe-core</title>
      <dc:creator>Vadym Liubyvyi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:22:52 +0000</pubDate>
      <link>https://dev.to/liubyvyi/accessibility-testing-with-playwright-and-axe-core-3n6b</link>
      <guid>https://dev.to/liubyvyi/accessibility-testing-with-playwright-and-axe-core-3n6b</guid>
      <description>&lt;p&gt;Playwright drives a real browser, and &lt;strong&gt;axe-core&lt;/strong&gt; is the open-source rule engine behind most automated accessibility checkers. Together they turn WCAG checks into ordinary tests: a page that gains a button without a name or a text with low contrast breaks the build before it reaches customers. This guide shows working examples with &lt;strong&gt;@axe-core/playwright&lt;/strong&gt;, the options for React components and Storybook, CI tips, and the limits you should know about.&lt;/p&gt;

&lt;p&gt;The examples follow the official &lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;Playwright accessibility testing guide&lt;/a&gt; and the &lt;a href="https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md" rel="noopener noreferrer"&gt;@axe-core/playwright README&lt;/a&gt;. We use the same engine in our own scanner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @playwright/test @axe-core/playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package version follows the axe-core version it ships. Its README notes that it does not follow semantic versioning, so pin the version and update it on purpose: a new axe release can add rules that find new problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first WCAG test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AxeBuilder&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@axe-core/playwright&lt;/span&gt;&lt;span class="dl"&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;WCAG_AA&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;wcag2a&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;wcag2aa&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;wcag21a&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;wcag21aa&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;wcag22aa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home page has no detectable WCAG A/AA violations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;withTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WCAG_AA&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&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;code&gt;withTags&lt;/code&gt; limits the run to the rules tagged with these WCAG versions and levels. Without it, axe also runs its best-practice rules, which are useful but are not WCAG requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the states people actually see
&lt;/h2&gt;

&lt;p&gt;A page is more than its first render. Open the menu, the cart drawer or the cookie dialog first, then scan just that part with &lt;code&gt;include&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile menu is accessible when open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#navigation-menu&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="nf"&gt;withTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WCAG_AA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&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;Good candidates on a shop: product page with options selected, cart with items, checkout with validation errors shown, the account login form. Using &lt;code&gt;getByRole&lt;/code&gt; in your tests is a small accessibility test on its own: if Playwright cannot find the button by its role and name, a screen reader user probably cannot either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known issues without switching tests off
&lt;/h2&gt;

&lt;p&gt;Real projects have problems you cannot fix today, for example inside a third-party widget. The README offers three tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#third-party-chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// skip an element&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableRules&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;color-contrast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;// skip a rule (use rarely)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WCAG_AA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better pattern from the Playwright guide is a &lt;strong&gt;snapshot of fingerprints&lt;/strong&gt;: you record which rules fail on which elements, and the test fails only when something new appears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fingerprints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&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="na"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&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="p"&gt;}));&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fingerprints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSnapshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes from the README: &lt;code&gt;include&lt;/code&gt; and &lt;code&gt;exclude&lt;/code&gt; take one selector per call (chain them for several), and calling &lt;code&gt;withTags&lt;/code&gt;, &lt;code&gt;withRules&lt;/code&gt;, &lt;code&gt;disableRules&lt;/code&gt; or &lt;code&gt;options&lt;/code&gt; again overrides the earlier call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail on what matters first
&lt;/h2&gt;

&lt;p&gt;If the backlog is large, start with the most severe problems. Every violation carries an &lt;code&gt;impact&lt;/code&gt; of minor, moderate, serious or critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blocking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serious&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;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impact&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach the full result to the report so whoever fixes it sees every element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside test('…', async ({ page }, testInfo) =&amp;gt; { … })&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accessibility-scan-results&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Playwright guide also shows how to wrap &lt;code&gt;AxeBuilder&lt;/code&gt; in a &lt;strong&gt;test fixture&lt;/strong&gt;, so every test gets the same tags and exclusions from one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  React components: jest-axe and vitest-axe
&lt;/h2&gt;

&lt;p&gt;For component tests that render into JSDOM, &lt;a href="https://github.com/NickColley/jest-axe" rel="noopener noreferrer"&gt;jest-axe&lt;/a&gt; and &lt;a href="https://github.com/chaance/vitest-axe" rel="noopener noreferrer"&gt;vitest-axe&lt;/a&gt; add a &lt;code&gt;toHaveNoViolations&lt;/code&gt; matcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;axe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest-axe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest-axe/extend-expect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders an accessible form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SignupForm&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;axe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveNoViolations&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;They are fast and catch missing labels and names early. Two limits from their READMEs: jest-axe turns off &lt;strong&gt;colour contrast&lt;/strong&gt; checks because they do not work in JSDOM, and vitest-axe does not work with the happy-dom environment. Contrast, layout, zoom and focus need a real browser, so keep the Playwright tests for full pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storybook
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://storybook.js.org/docs/writing-tests/accessibility-testing" rel="noopener noreferrer"&gt;Storybook accessibility addon&lt;/a&gt; runs axe-core on every story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx storybook add @storybook/addon-a11y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;parameters.a11y.test&lt;/code&gt; to &lt;code&gt;'error'&lt;/code&gt; for stories that must stay clean: violations then fail the test in the Storybook UI and in CI. &lt;code&gt;'todo'&lt;/code&gt; shows them as warnings only, and &lt;code&gt;'off'&lt;/code&gt; skips the automatic check.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run in the same pipeline as your other tests.&lt;/strong&gt; The Playwright CI guide uses &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npx playwright install --with-deps&lt;/code&gt; and &lt;code&gt;npx playwright test&lt;/code&gt;, and uploads the &lt;code&gt;playwright-report/&lt;/code&gt; folder as an artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test a built, production-like page&lt;/strong&gt;, not a dev server with debugging overlays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a short list of key URLs&lt;/strong&gt; — home, a category, a product, cart, checkout, login, a content page — instead of crawling everything on every commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the axe version pinned&lt;/strong&gt; and update it in its own pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not hide failures with &lt;code&gt;disableRules&lt;/code&gt;&lt;/strong&gt; for long; prefer &lt;code&gt;exclude&lt;/code&gt; for one element or the snapshot pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What automated tests cannot tell you
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/dequelabs/axe-core" rel="noopener noreferrer"&gt;axe-core README&lt;/a&gt; says that you can find on average &lt;strong&gt;57% of WCAG issues&lt;/strong&gt; automatically, and the Playwright guide is equally clear that automated testing cannot detect all types of WCAG violations. An empty &lt;code&gt;violations&lt;/code&gt; array does not mean a page meets WCAG 2.2 AA. You still need a keyboard walk-through, a zoom and reflow check and a screen reader test. Our &lt;a href="https://pandus.online/en/how-to-test-wcag" rel="noopener noreferrer"&gt;WCAG testing guide&lt;/a&gt; describes that part, and the &lt;a href="https://pandus.online/en/wcag-2-2-checklist-online-shops" rel="noopener noreferrer"&gt;WCAG 2.2 checklist&lt;/a&gt; marks which points need a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Pandus fits
&lt;/h2&gt;

&lt;p&gt;Tests in CI protect the code you change. They do not see what happens on the live site afterwards: a theme update, a new app or plugin, a marketing banner, a product description pasted from a supplier. Pandus checks the &lt;strong&gt;production site&lt;/strong&gt;: every page each week, with the same axe-core engine in a real browser, and a report a shop owner can read.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://pandus.online/en/accessibility-testing-api" rel="noopener noreferrer"&gt;API&lt;/a&gt; starts checks and returns results as JSON, with webhooks when a check finishes or new issues appear — useful after a deployment.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://pandus.online/en/accessibility-checker-browser-extension" rel="noopener noreferrer"&gt;browser extension&lt;/a&gt; checks a page as your browser shows it, including staging and pages behind a login.&lt;/li&gt;
&lt;li&gt;The free &lt;a href="https://pandus.online/en/wcag-checker" rel="noopener noreferrer"&gt;WCAG checker&lt;/a&gt; checks one public page without an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pandus is an automated check, not a full audit, and not legal advice. A comparison of other tools is in &lt;a href="https://pandus.online/en/accessibility-testing-tools" rel="noopener noreferrer"&gt;accessibility testing tools&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I test accessibility with Playwright?
&lt;/h3&gt;

&lt;p&gt;Install @axe-core/playwright, open the page in a Playwright test, run new AxeBuilder({ page }).analyze() and assert that the violations array is empty. Limit the run to WCAG rules with withTags, for example wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a passing axe test mean the page meets WCAG?
&lt;/h3&gt;

&lt;p&gt;No. Automated rules find a part of WCAG problems only. Deque, the maker of axe-core, puts it at about 57% of issues on average. Keyboard flow, meaningful alternative text, focus order and understandable errors still need a person.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use jest-axe or vitest-axe instead?
&lt;/h3&gt;

&lt;p&gt;Yes, for components rendered in JSDOM. They are fast, but jest-axe turns off colour contrast checks because they do not work in JSDOM. Use Playwright for full pages in a real browser, where contrast, layout and focus can be checked.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test Storybook components for accessibility?
&lt;/h3&gt;

&lt;p&gt;Add @storybook/addon-a11y. It runs axe-core on every story; with parameters.a11y.test set to error, violations fail the test in the Storybook UI and in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the build fail on every violation?
&lt;/h3&gt;

&lt;p&gt;Start by failing on serious and critical issues and keep a snapshot of known ones, so new problems break the build while old ones are fixed step by step. Tighten the rule once the backlog is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Playwright, Accessibility testing: &lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;https://playwright.dev/docs/accessibility-testing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playwright, Setting up CI: &lt;a href="https://playwright.dev/docs/ci-intro" rel="noopener noreferrer"&gt;https://playwright.dev/docs/ci-intro&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Deque, @axe-core/playwright README: &lt;a href="https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md" rel="noopener noreferrer"&gt;https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Deque, axe-core README: &lt;a href="https://github.com/dequelabs/axe-core" rel="noopener noreferrer"&gt;https://github.com/dequelabs/axe-core&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;jest-axe README: &lt;a href="https://github.com/NickColley/jest-axe" rel="noopener noreferrer"&gt;https://github.com/NickColley/jest-axe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vitest-axe README: &lt;a href="https://github.com/chaance/vitest-axe" rel="noopener noreferrer"&gt;https://github.com/chaance/vitest-axe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Storybook, Accessibility tests: &lt;a href="https://storybook.js.org/docs/writing-tests/accessibility-testing" rel="noopener noreferrer"&gt;https://storybook.js.org/docs/writing-tests/accessibility-testing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://pandus.online/en/accessibility-testing-playwright" rel="noopener noreferrer"&gt;pandus.online&lt;/a&gt;. Pandus is an automated accessibility check, not a full audit, and not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>playwright</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>APCA vs WCAG contrast: which one should you use?</title>
      <dc:creator>Vadym Liubyvyi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:22:47 +0000</pubDate>
      <link>https://dev.to/liubyvyi/apca-vs-wcag-contrast-which-one-should-you-use-30lc</link>
      <guid>https://dev.to/liubyvyi/apca-vs-wcag-contrast-which-one-should-you-use-30lc</guid>
      <description>&lt;p&gt;Designers who check colours sooner or later meet two numbers: the &lt;strong&gt;WCAG contrast ratio&lt;/strong&gt; (4.5:1, 3:1) and an &lt;strong&gt;APCA value&lt;/strong&gt; such as Lc 75. They do not always agree. This page explains how the WCAG 2 formula works, what APCA is and what its status really is, and how Windows high contrast mode relates to both.&lt;/p&gt;

&lt;p&gt;The short answer for anyone who has to meet a requirement: &lt;strong&gt;laws and standards use the WCAG 2 contrast ratio.&lt;/strong&gt; This is general information, not legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WCAG 2 formula, step by step
&lt;/h2&gt;

&lt;p&gt;WCAG 2.2 defines contrast in two steps, both in its &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;glossary&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Relative luminance&lt;/strong&gt; — how bright a colour is, from 0 for the darkest black to 1 for the lightest white. For each sRGB channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C_srgb = C_8bit / 255
C = C_srgb / 12.92                         if C_srgb &amp;lt;= 0.04045
C = ((C_srgb + 0.055) / 1.055) ^ 2.4       otherwise

L = 0.2126 * R + 0.7152 * G + 0.0722 * B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights show that the eye is far more sensitive to green than to blue. (Before May 2021 the WCAG text used 0.03928 as the threshold; the W3C notes this has no practical effect.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Contrast ratio&lt;/strong&gt; — with L1 the luminance of the lighter colour and L2 of the darker one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contrast ratio = (L1 + 0.05) / (L2 + 0.05)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result goes from &lt;strong&gt;1:1&lt;/strong&gt; (the same colour) to &lt;strong&gt;21:1&lt;/strong&gt; (black on white). An example: grey &lt;code&gt;#767676&lt;/code&gt; has a luminance of about 0.181, so on white it reaches (1 + 0.05) / (0.181 + 0.05) ≈ &lt;strong&gt;4.54:1&lt;/strong&gt; and passes the 4.5:1 minimum. One step lighter, &lt;code&gt;#777777&lt;/code&gt;, gives about 4.48:1 and fails — W3C says the values &lt;strong&gt;must not be rounded&lt;/strong&gt;, so 4.499:1 does not meet 4.5:1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thresholds
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Required ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pandus.online/en/wcag/1-4-3-contrast-minimum" rel="noopener noreferrer"&gt;1.4.3 Contrast (Minimum)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;4.5:1 text, 3:1 large text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pandus.online/en/wcag/1-4-11-non-text-contrast" rel="noopener noreferrer"&gt;1.4.11 Non-text Contrast&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;3:1 for icons, field borders, focus indicators and other UI parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.6 Contrast (Enhanced)&lt;/td&gt;
&lt;td&gt;AAA&lt;/td&gt;
&lt;td&gt;7:1 text, 4.5:1 large text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Large text is at least 18 point (24 CSS pixels) or 14 point bold (about 18.66 CSS pixels). The W3C explains the 4.5:1 value as compensating for the loss of contrast sensitivity that comes with about 20/40 vision. Check any pair with our &lt;a href="https://pandus.online/en/color-contrast-checker" rel="noopener noreferrer"&gt;contrast checker&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits of the WCAG 2 formula
&lt;/h2&gt;

&lt;p&gt;The formula is simple and testable, which is why it works for regulation. It also has limits that W3C's own Understanding document mentions: it does not look at &lt;strong&gt;font weight&lt;/strong&gt;, and thin or unusual fonts can look much fainter than their nominal colour because of anti-aliasing — "a contrast ratio that nominally passes … but has a much lower contrast in practice". The recommendation there is to use heavier fonts or a colour pair that exceeds the minimum. The formula also assumes sRGB colours.&lt;/p&gt;

&lt;p&gt;The formula is also symmetric: swapping text and background gives the same ratio. The APCA project goes further and states that WCAG 2 contrast math "does not accurately model human visual perception nor visual impairments" — the starting point for APCA.&lt;/p&gt;

&lt;h2&gt;
  
  
  What APCA is
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Accessible Perceptual Contrast Algorithm&lt;/strong&gt; is described in its &lt;a href="https://github.com/Myndex/apca-w3" rel="noopener noreferrer"&gt;official repository&lt;/a&gt; as an assessment method for predicting the perceived contrast between sRGB colours on a monitor, developed for the W3 "Silver" / WCAG 3 work. The differences from WCAG 2 in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order matters.&lt;/strong&gt; The first colour is the text, the second the background; the result is signed, and the sign shows the polarity (dark on light or light on dark).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A different scale.&lt;/strong&gt; The output is an &lt;strong&gt;Lc&lt;/strong&gt; value, roughly from −108 to 106, not a ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size and weight are part of the answer.&lt;/strong&gt; A lookup function returns, for an Lc value, the font sizes needed at weights 100 to 900. The README gives rules such as a maximum of Lc 90 for fonts larger than 24 px at weight 300 or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beta.&lt;/strong&gt; The library (&lt;code&gt;apca-w3&lt;/code&gt; on npm) is in public beta validation, and the README calls the related draft criterion a work in progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is APCA part of WCAG 3?
&lt;/h2&gt;

&lt;p&gt;Not as a final decision. Two facts, both checked on the official sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The APCA repository says APCA is &lt;strong&gt;being evaluated&lt;/strong&gt; as a replacement for the WCAG 2 contrast math for future standards.&lt;/li&gt;
&lt;li&gt;The current &lt;a href="https://www.w3.org/TR/wcag-3.0/" rel="noopener noreferrer"&gt;WCAG 3 Working Draft&lt;/a&gt;, dated 10 September 2026, says: &lt;strong&gt;"The contrast algorithm used in WCAG 3 is yet to be determined."&lt;/strong&gt; Its text contrast requirements have the status &lt;em&gt;developing&lt;/em&gt;, with the measure left open. The draft also says it is inappropriate to cite it as other than a work in progress.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So APCA is a candidate, WCAG 3 is a draft, and neither is normative. &lt;strong&gt;WCAG 2.2&lt;/strong&gt; is the current W3C Recommendation, and &lt;strong&gt;EN 301 549&lt;/strong&gt;, the European standard behind the &lt;a href="https://pandus.online/en/european-accessibility-act-guide" rel="noopener noreferrer"&gt;European Accessibility Act&lt;/a&gt;, refers to WCAG 2 success criteria. A colour pair that passes APCA but fails 4.5:1 still fails the requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  WCAG or APCA: a practical rule
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Meet the WCAG 2 ratio&lt;/strong&gt; for every text and every UI component. That is the requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use APCA, if you like, as a design aid&lt;/strong&gt; on top: for example to choose a heavier weight for thin text, or to notice that a pair that just passes 4.5:1 is still hard to read in light text on a dark background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not claim APCA results as conformance.&lt;/strong&gt; Reports, statements and tenders refer to WCAG 2.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Windows high contrast mode and forced colours
&lt;/h2&gt;

&lt;p&gt;"WCAG high contrast mode" is a common search, but it mixes two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Windows offers &lt;strong&gt;contrast themes&lt;/strong&gt; (Windows 11; called high contrast mode in Windows 10). Microsoft documents them under Settings → Accessibility → Contrast themes, with the shortcut Left Alt + Left Shift + Print Screen. Browsers then apply the user's limited palette to web pages. CSS detects this with the &lt;a href="https://www.w3.org/TR/mediaqueries-5/" rel="noopener noreferrer"&gt;&lt;code&gt;forced-colors&lt;/code&gt;&lt;/a&gt; media feature: &lt;code&gt;active&lt;/code&gt; means the user agent enforces a user-chosen limited colour palette, and only system colours are available to the author.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What WCAG says.&lt;/strong&gt; No WCAG 2.2 success criterion requires a high contrast mode or support for forced colours by name, and a site does not need to offer its own high contrast switch to meet AA. But your code can break the page for people who use such a mode, and then other criteria fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus.&lt;/strong&gt; W3C technique C40 warns that user agents commonly suppress &lt;code&gt;box-shadow&lt;/code&gt; in forced-color modes, so a focus indicator made only with a shadow disappears. C40 suggests combining it with &lt;code&gt;outline: 2px transparent solid&lt;/code&gt; for compatibility with forced-color modes — relevant for &lt;a href="https://pandus.online/en/wcag/2-4-7-focus-visible" rel="noopener noreferrer"&gt;2.4.7 Focus Visible&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icons and borders.&lt;/strong&gt; Icons drawn with background images, and controls distinguished only by a background colour, can vanish. This touches &lt;a href="https://pandus.online/en/wcag/1-4-11-non-text-contrast" rel="noopener noreferrer"&gt;1.4.11 Non-text Contrast&lt;/a&gt; and &lt;a href="https://pandus.online/en/wcag/1-4-1-use-of-color" rel="noopener noreferrer"&gt;1.4.1 Use of Color&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not assume more contrast.&lt;/strong&gt; The Media Queries specification notes that forced colours do not necessarily mean a preference for more contrast; for that, &lt;code&gt;prefers-contrast&lt;/code&gt; exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing a few key pages with a contrast theme switched on is good practice, even though no criterion names it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our checks do
&lt;/h2&gt;

&lt;p&gt;Our checks use the &lt;strong&gt;WCAG 2 contrast ratio&lt;/strong&gt;, as the requirements do: low text contrast at AA (1.4.3), and enhanced contrast at AAA (1.4.6), which our reports show apart from the AA results. We do not report APCA values. Run the free &lt;a href="https://pandus.online/en/wcag-checker" rel="noopener noreferrer"&gt;WCAG checker&lt;/a&gt; on a page, or test one pair with the &lt;a href="https://pandus.online/en/color-contrast-checker" rel="noopener noreferrer"&gt;contrast checker&lt;/a&gt;. Our checks are an automated check, not a full audit, and not legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I use WCAG or APCA for contrast?
&lt;/h3&gt;

&lt;p&gt;Use the WCAG 2 contrast ratio to meet requirements: it is what WCAG 2.2, EN 301 549 and the laws built on them refer to. APCA can be a useful second opinion in design, but it is not part of any normative standard today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is APCA part of WCAG 3?
&lt;/h3&gt;

&lt;p&gt;Not as a decision. APCA was developed as a candidate method for WCAG 3 and is in public beta. The current WCAG 3 Working Draft of September 2026 says the contrast algorithm is yet to be determined, and WCAG 3 itself is a draft, not a standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the WCAG contrast ratio calculated?
&lt;/h3&gt;

&lt;p&gt;Convert both colours to relative luminance: linearise each sRGB channel, then L = 0.2126 R + 0.7152 G + 0.0722 B. The ratio is (L1 + 0.05) / (L2 + 0.05), with L1 the lighter colour. It ranges from 1:1 to 21:1 and is not rounded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does WCAG require support for Windows high contrast mode?
&lt;/h3&gt;

&lt;p&gt;No WCAG criterion names high contrast or forced colours mode. But your own CSS can break the page in that mode, for example a focus ring made only with box-shadow, which browsers commonly remove there. Testing in a forced colours mode is good practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a colour pair pass WCAG and still be hard to read?
&lt;/h3&gt;

&lt;p&gt;Yes. The WCAG formula does not account for font weight, and thin fonts with anti-aliasing can look fainter than their colour suggests. The W3C recommends heavier fonts or more contrast than the minimum in that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;W3C, WCAG 2.2 (definitions of relative luminance and contrast ratio): &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/WCAG22/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C, Understanding 1.4.3 Contrast (Minimum): &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C, WCAG 3.0 Working Draft: &lt;a href="https://www.w3.org/TR/wcag-3.0/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/wcag-3.0/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;APCA, apca-w3 repository: &lt;a href="https://github.com/Myndex/apca-w3" rel="noopener noreferrer"&gt;https://github.com/Myndex/apca-w3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C, Technique C40, two-colour focus indicator: &lt;a href="https://www.w3.org/WAI/WCAG22/Techniques/css/C40" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/WCAG22/Techniques/css/C40&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C, Media Queries Level 5 (forced-colors): &lt;a href="https://www.w3.org/TR/mediaqueries-5/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/mediaqueries-5/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft, Change color contrast in Windows: &lt;a href="https://support.microsoft.com/en-us/windows/change-color-contrast-in-windows-fedc744c-90ac-69df-aed5-c8a90125e696" rel="noopener noreferrer"&gt;https://support.microsoft.com/en-us/windows/change-color-contrast-in-windows-fedc744c-90ac-69df-aed5-c8a90125e696&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://pandus.online/en/apca-vs-wcag-contrast" rel="noopener noreferrer"&gt;pandus.online&lt;/a&gt;. Pandus is an automated accessibility check, not a full audit, and not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>css</category>
      <category>design</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
