<?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: Denis Omerovic</title>
    <description>The latest articles on DEV Community by Denis Omerovic (@chille87).</description>
    <link>https://dev.to/chille87</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%2F3016792%2F2823df19-1334-438f-a4ad-f592800a3f19.jpg</url>
      <title>DEV Community: Denis Omerovic</title>
      <link>https://dev.to/chille87</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chille87"/>
    <language>en</language>
    <item>
      <title>React Accessibility Issues That Audits Keep Finding in Next.js</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:14:45 +0000</pubDate>
      <link>https://dev.to/chille87/react-accessibility-issues-that-audits-keep-finding-in-nextjs-27l0</link>
      <guid>https://dev.to/chille87/react-accessibility-issues-that-audits-keep-finding-in-nextjs-27l0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/react-nextjs-accessibility-issues" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;You inherit a React or Next.js build. The markup looks tidy, the components are typed, and Lighthouse is green. Then somebody tabs through it with a screen reader running, and the page changes without a word, a menu cannot be opened from the keyboard, and the "Saved" message never gets read out.&lt;/p&gt;

&lt;p&gt;None of that is a markup typo. These are failures that come from how single page apps work: the browser no longer loads a new page, so everything it used to do for free on a page load is now your code's job. This guide lists what audits of React and Next.js sites keep finding, the WCAG criterion each one breaks, and the fix.&lt;/p&gt;

&lt;h2&gt;1. Route changes nobody hears&lt;/h2&gt;

&lt;p&gt;On a normal website, clicking a link loads a new document. The screen reader announces the new page title, and focus starts at the top. With client side routing, the URL changes and the content swaps, but the browser sees no page load. A screen reader user who pressed Enter on a link can be left on the same spot with no idea anything happened.&lt;/p&gt;

&lt;h3&gt;What Next.js does for you&lt;/h3&gt;

&lt;p&gt;Next.js ships a route announcer by default. Its documentation says it "looks for the page name to announce by first inspecting &lt;code&gt;document.title&lt;/code&gt;, then the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; element, and finally the URL pathname" (&lt;a href="https://nextjs.org/docs/architecture/accessibility" rel="noopener noreferrer"&gt;Next.js docs, Accessibility&lt;/a&gt;). So the most common Next.js failure here is not a missing announcer. It is every page sharing one title, such as "Acme" on every route, so the announcer reads the same word after every click.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Broken: every route has the same title, so every navigation announces "Acme"
export const metadata = { title: "Acme" };

// Fixed: a unique title per page, set in each page or layout (App Router)
export const metadata = { title: "Pricing - Acme" };&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A plain React app with a client side router does not get an announcer from React itself. Check what your router provides, and if it provides nothing, add a visually hidden live region that receives the new page title after each navigation.&lt;/p&gt;

&lt;h3&gt;Where focus should land&lt;/h3&gt;

&lt;p&gt;The Next.js docs describe an announcement, not a focus move. Announcing the page helps, but focus can still sit on a link in a navigation that is no longer relevant, or fall back to the top of the document. WCAG 2.4.3 Focus Order asks that focus moves in an order that "preserves meaning and operability" (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html" rel="noopener noreferrer"&gt;W3C, Understanding 2.4.3&lt;/a&gt;). A common, predictable fix is to move focus to the new page's main heading after a route change:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Fixed: after navigation, move focus to the page heading
"use client";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";

export function PageHeading({ children }) {
  const ref = useRef(null);
  const pathname = usePathname();
  useEffect(() =&amp;gt; { ref.current?.focus(); }, [pathname]);
  return &amp;lt;h1 ref={ref} tabIndex={-1}&amp;gt;{children}&amp;lt;/h1&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;tabIndex={-1}&lt;/code&gt; lets the heading take focus from script without adding it to the tab order. Pick one rule and apply it on every route.&lt;/p&gt;

&lt;h2&gt;2. div onClick and the button it should have been&lt;/h2&gt;

&lt;p&gt;JSX makes it easy to put &lt;code&gt;onClick&lt;/code&gt; on anything, so audits keep finding clickable &lt;code&gt;div&lt;/code&gt;s, &lt;code&gt;span&lt;/code&gt;s and icons. A mouse user cannot tell the difference. A keyboard user cannot reach the element, and a screen reader does not announce it as a control. That fails 2.1.1 Keyboard and 4.1.2 Name, Role, Value, which requires that for every user interface component "the name and role can be programmatically determined" (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html" rel="noopener noreferrer"&gt;W3C, Understanding 4.1.2&lt;/a&gt;).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Broken: no focus, no Enter or Space, announced as plain text
&amp;lt;div className="card__toggle" onClick={toggle}&amp;gt;
  &amp;lt;ChevronIcon /&amp;gt;
&amp;lt;/div&amp;gt;

// Fixed: a real button, with a name and its state
&amp;lt;button type="button" className="card__toggle" onClick={toggle}
        aria-expanded={open} aria-label="Show details"&amp;gt;
  &amp;lt;ChevronIcon aria-hidden="true" /&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Adding &lt;code&gt;role="button"&lt;/code&gt; and &lt;code&gt;tabIndex={0}&lt;/code&gt; to the div is the long way round: you then have to handle Enter and Space yourself, and the next developer will forget one of them. A native &lt;code&gt;button&lt;/code&gt; does it all. If the thing goes somewhere, it should be a link instead; our guide on &lt;a href="https://getaccessguard.com/posts/button-vs-link-accessibility" rel="noopener noreferrer"&gt;button vs link accessibility&lt;/a&gt; has the rule for choosing.&lt;/p&gt;

&lt;p&gt;Start with shared components: one &lt;code&gt;&amp;lt;IconButton&amp;gt;&lt;/code&gt; built on a div repeats the failure on every page, and one fix clears them all.&lt;/p&gt;

&lt;h2&gt;3. Styling by ARIA state, not by a separate state variable&lt;/h2&gt;

&lt;p&gt;A quieter bug: the component tracks &lt;code&gt;isOpen&lt;/code&gt; in React state, sets a class from it, and sets &lt;code&gt;aria-expanded&lt;/code&gt; from something else, or forgets to set it at all. The panel looks open, but a screen reader says "collapsed". 4.1.2 also requires that states set by the user can be programmatically determined, so a mismatch is a failure, not a nicety.&lt;/p&gt;

&lt;p&gt;The fix is to make the ARIA attribute the single source the styling reads from. If the CSS keys off &lt;code&gt;aria-expanded&lt;/code&gt;, the visual state and the announced state cannot drift apart, because a missing attribute shows up as a visual bug that someone will notice.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: the look depends on a class the ARIA attribute never sees */
.disclosure--open .disclosure__icon { rotate: 180deg; }

/* Fixed: the look depends on the attribute screen readers read */
.disclosure__button[aria-expanded="true"] .disclosure__icon { rotate: 180deg; }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The same idea works with class-variance-authority (CVA) and Tailwind, which is a common setup in Next.js projects. Instead of a CVA variant like &lt;code&gt;open: { true: "rotate-180" }&lt;/code&gt; driven by a prop, put the state on the element and let the classes respond to it. Tailwind has variants for ARIA attributes, such as &lt;code&gt;aria-expanded:&lt;/code&gt; and &lt;code&gt;aria-selected:&lt;/code&gt;, and &lt;code&gt;data-*&lt;/code&gt; variants for libraries that expose a &lt;code&gt;data-state&lt;/code&gt; attribute (&lt;a href="https://tailwindcss.com/docs/hover-focus-and-other-states" rel="noopener noreferrer"&gt;Tailwind docs, states&lt;/a&gt;).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Broken: the variant and aria-expanded are set separately and can disagree
const trigger = cva("trigger", { variants: { open: { true: "bg-muted" } } });
&amp;lt;button className={trigger({ open: isOpen })}&amp;gt;Menu&amp;lt;/button&amp;gt;

// Fixed: one value sets aria-expanded, and the classes read the attribute
const trigger = cva("trigger aria-expanded:bg-muted");
&amp;lt;button className={trigger()} aria-expanded={isOpen}&amp;gt;Menu&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;4. Modals, portals and the focus that escapes them&lt;/h2&gt;

&lt;p&gt;React modals are usually rendered with &lt;code&gt;createPortal&lt;/code&gt; into the end of &lt;code&gt;document.body&lt;/code&gt;. The React docs warn that with portals "you may need to manage keyboard focus so that the user can move the focus in and out of the portal in a natural way", and point to the WAI-ARIA modal pattern (&lt;a href="https://react.dev/reference/react-dom/createPortal" rel="noopener noreferrer"&gt;React docs, createPortal&lt;/a&gt;). Audits keep finding three gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus does not move in.&lt;/strong&gt; The dialog opens, but focus stays on the button behind it. The WAI-ARIA pattern says focus moves to an element inside the dialog when it opens (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/" rel="noopener noreferrer"&gt;W3C APG, dialog (modal) pattern&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus escapes.&lt;/strong&gt; Tab walks out of the dialog into the page behind it. The pattern says Tab and Shift+Tab cycle inside the dialog, and Escape closes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus does not come back.&lt;/strong&gt; On close, focus drops to the top of the page instead of returning to the element that opened the dialog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element handles more of this than most custom components. MDN notes that with &lt;code&gt;showModal()&lt;/code&gt;, the rest of the page is made inert by the browser, Escape closes the dialog, and focus moves to the first focusable element inside it (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog" rel="noopener noreferrer"&gt;MDN, dialog element&lt;/a&gt;). Returning focus on close is still worth testing by hand.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Broken: a div overlay in a portal, with no focus handling
{open &amp;amp;&amp;amp; createPortal(&amp;lt;div className="modal"&amp;gt;{children}&amp;lt;/div&amp;gt;, document.body)}

// Fixed: native dialog opened with showModal, focus returned on close
function Modal({ open, onClose, title, children }) {
  const ref = useRef(null);
  const opener = useRef(null);
  useEffect(() =&amp;gt; {
    if (open) { opener.current = document.activeElement; ref.current.showModal(); }
    else if (ref.current?.open) { ref.current.close(); opener.current?.focus(); }
  }, [open]);
  return (
    &amp;lt;dialog ref={ref} onClose={onClose} aria-labelledby="modal-title"&amp;gt;
      &amp;lt;h2 id="modal-title"&amp;gt;{title}&amp;lt;/h2&amp;gt;
      {children}
    &amp;lt;/dialog&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One more portal detail that surprises people: React events from a portal bubble through the React tree, not the DOM tree, so a click inside the modal can trigger an &lt;code&gt;onClick&lt;/code&gt; on a parent component that is nowhere near it on the page (&lt;a href="https://react.dev/reference/react-dom/createPortal" rel="noopener noreferrer"&gt;React docs, createPortal caveats&lt;/a&gt;). If closing a modal also fires a click behind it, this is usually why. Our &lt;a href="https://getaccessguard.com/posts/accessible-modals-how-to-build-dialogs-that-dont-trap-confuse-or-exclude-users" rel="noopener noreferrer"&gt;guide to accessible modals&lt;/a&gt; covers the dialog pattern in full.&lt;/p&gt;

&lt;h2&gt;5. Live regions that are added too late&lt;/h2&gt;

&lt;p&gt;React apps are full of async state: "Saving...", "Added to cart", "3 results", form errors after submit. The usual fix is &lt;code&gt;aria-live&lt;/code&gt;, and the usual bug is rendering the live region at the same moment as its message:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Broken: the region and its text appear in the same render, often silent
{saved &amp;amp;&amp;amp; &amp;lt;p aria-live="polite"&amp;gt;Changes saved&amp;lt;/p&amp;gt;}

// Fixed: the region is always there, only its text changes
&amp;lt;p role="status" aria-live="polite"&amp;gt;{saved ? "Changes saved" : ""}&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;MDN explains why: assistive technologies "will generally only announce dynamic changes in the content of a live region", so you should "establish the live region before updating its content", and "the most reliable way to ensure that live regions are registered is to include them in the initial markup" (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions" rel="noopener noreferrer"&gt;MDN, ARIA live regions&lt;/a&gt;). In React terms: render the container unconditionally, and change only what is inside it. MDN also notes that &lt;code&gt;role="alert"&lt;/code&gt; gets special handling and is generally announced even when injected, which is why it works for urgent errors but should not be used for routine status. Our guide to &lt;a href="https://getaccessguard.com/posts/aria-live-regions-role-status-and-role-alert" rel="noopener noreferrer"&gt;role="status" and role="alert"&lt;/a&gt; goes deeper.&lt;/p&gt;

&lt;h2&gt;6. What jest-axe and @axe-core/playwright catch, and what they miss&lt;/h2&gt;

&lt;p&gt;Adding axe to your tests is worth doing. It catches missing names, invalid ARIA, missing labels and similar problems on every build. Deque, which maintains axe-core, reports that 57.38% of issues found in first-time audits of its customers could be found with automated testing (&lt;a href="https://www.deque.com/automated-accessibility-testing-coverage/" rel="noopener noreferrer"&gt;Deque, automated testing coverage&lt;/a&gt;). The rest needs a person.&lt;/p&gt;

&lt;p&gt;Two tool details matter for React projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;jest-axe runs in JSDOM, which does not render.&lt;/strong&gt; Its README says "color contrast checks do not work in JSDOM so are turned off in jest-axe" (&lt;a href="https://github.com/NickColley/jest-axe" rel="noopener noreferrer"&gt;jest-axe on GitHub&lt;/a&gt;). Component tests will never flag contrast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@axe-core/playwright runs in a real browser&lt;/strong&gt;, so it sees rendered styles. Playwright's own docs still say "automated testing cannot detect all types of WCAG violations" (&lt;a href="https://playwright.dev/docs/accessibility-testing" rel="noopener noreferrer"&gt;Playwright docs, accessibility testing&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four things from this guide that a passing axe run does not prove:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where focus goes after a route change.&lt;/strong&gt; axe checks one state of the page, not what happens between two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether a live region actually speaks.&lt;/strong&gt; axe can see the attribute, not whether the region existed before the text arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether a modal traps and returns focus.&lt;/strong&gt; That is behaviour across key presses, which needs a scripted or manual test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether a clickable div is a control.&lt;/strong&gt; Click handlers attached by React are not visible as HTML attributes, so a rule that reads the markup cannot see them. Look for them in code review, or use a check that tests the rendered page for clickable elements that are not controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the first three, a short &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;keyboard test by hand&lt;/a&gt; on each key template finds most of it in minutes.&lt;/p&gt;

&lt;h2&gt;How to explain this to a client or a manager&lt;/h2&gt;

&lt;p&gt;Keep it to one sentence per finding: "When someone navigates the site with a keyboard or a screen reader, the page changes without telling them", "the menu button cannot be opened without a mouse", "the save confirmation is shown but never read out". Then say where the fix lives. Most of these are fixed once in a shared component or layout, not page by page, which makes the estimate smaller than the finding list suggests.&lt;/p&gt;

&lt;p&gt;Be clear that automated checks, ours included, find many problems but cannot confirm a site conforms to WCAG. The behaviours in this guide need a person with a keyboard to sign them off.&lt;/p&gt;

&lt;h2&gt;Checklist before you audit a React or Next.js build&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every route has a unique, descriptive &lt;code&gt;title&lt;/code&gt;, and you know whether your router announces changes.&lt;/li&gt;
&lt;li&gt;One agreed rule for where focus lands after navigation, applied everywhere.&lt;/li&gt;
&lt;li&gt;Search the code for &lt;code&gt;onClick&lt;/code&gt; on &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;span&lt;/code&gt;, &lt;code&gt;li&lt;/code&gt; and &lt;code&gt;svg&lt;/code&gt;, and start with shared components.&lt;/li&gt;
&lt;li&gt;Styles for open, selected and pressed states read &lt;code&gt;aria-*&lt;/code&gt; or &lt;code&gt;data-state&lt;/code&gt; attributes, not a separate class.&lt;/li&gt;
&lt;li&gt;Modals move focus in, keep it in, close on Escape and return it to the opener.&lt;/li&gt;
&lt;li&gt;Live regions are rendered on first load and only their text changes.&lt;/li&gt;
&lt;li&gt;axe runs in a real browser in CI, and a person tests the behaviour it cannot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see what a rendered page fails right now, &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;run a free accessibility scan&lt;/a&gt;: AccessGuard runs automated WCAG 2.2 AA checks in a real browser, so it sees what your React code actually renders. For focused checks, &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;check a page for controls a keyboard cannot reach or see&lt;/a&gt;, and &lt;a href="https://getaccessguard.com/checks/aria" rel="noopener noreferrer"&gt;check a page for broken ARIA&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>EAA Accessibility Statement Requirements: What to Publish</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:14:15 +0000</pubDate>
      <link>https://dev.to/chille87/eaa-accessibility-statement-requirements-what-to-publish-olp</link>
      <guid>https://dev.to/chille87/eaa-accessibility-statement-requirements-what-to-publish-olp</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/eaa-accessibility-statement-requirements" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;A client sells online in the EU, someone has read that the European Accessibility Act needs "an accessibility statement", and the job lands on whoever looks after the website. Then the search starts for the official template, and there isn't one. The EAA tells a service provider what information to publish, not what the page has to look like.&lt;/p&gt;

&lt;p&gt;This guide covers what the directive actually asks for, a structure you can reuse on any site, how to describe conformance without promising more than your testing supports, and how to keep the statement true after the next release.&lt;/p&gt;

&lt;h2&gt;What the EAA actually asks for&lt;/h2&gt;

&lt;p&gt;The obligation sits in two places in &lt;a href="https://eur-lex.europa.eu/eli/dir/2019/882/oj" rel="noopener noreferrer"&gt;Directive (EU) 2019/882&lt;/a&gt;. Article 13(2) says service providers "shall prepare the necessary information in accordance with Annex V and shall explain how the services meet the applicable accessibility requirements." The same paragraph adds three conditions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the information is available to the public "in written and oral format";&lt;/li&gt;
  &lt;li&gt;it is available "in a manner which is accessible to persons with disabilities";&lt;/li&gt;
  &lt;li&gt;it is kept "for as long as the service is in operation".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Annex V then says where it goes and what it contains. The provider includes the information "in the general terms and conditions, or equivalent document". Where applicable, that information contains:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;a general description of the service in accessible formats;&lt;/li&gt;
  &lt;li&gt;descriptions and explanations needed to understand how the service operates;&lt;/li&gt;
  &lt;li&gt;a description of how the relevant accessibility requirements in Annex I are met by the service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Annex V point 3 adds one more thing that most published statements skip: the provider "shall provide information demonstrating that the service delivery process and its monitoring ensure compliance". In plain words, say how you keep it accessible, not only how accessible it was on the day you checked.&lt;/p&gt;

&lt;p&gt;For a website, the Annex I requirement you are describing is the one in Section III: making websites and mobile apps "accessible in a consistent and adequate way by making them perceivable, operable, understandable and robust". Those are the four principles of &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;WCAG&lt;/a&gt;, which is why every statement ends up talking about WCAG criteria.&lt;/p&gt;

&lt;p&gt;Three things the directive does not do: it does not prescribe a page title, a fixed set of headings, or a conformance label. The measures have applied since 28 June 2025 (Article 31(2)), and each member state's transposing law can add its own detail, so check the national law for every country the service is provided in. Microenterprises providing services are exempt from the service requirements under Article 4(5); the &lt;a href="https://getaccessguard.com/posts/does-the-european-accessibility-act-apply-to-your-business" rel="noopener noreferrer"&gt;guide to whether the EAA applies&lt;/a&gt; walks through that test.&lt;/p&gt;

&lt;h3&gt;Why people expect a template&lt;/h3&gt;

&lt;p&gt;Public sector sites in the EU do have one. &lt;a href="https://eur-lex.europa.eu/eli/dec_impl/2018/1523/oj" rel="noopener noreferrer"&gt;Commission Implementing Decision (EU) 2018/1523&lt;/a&gt; sets a model accessibility statement under the Web Accessibility Directive, with mandatory sections for compliance status, non-accessible content, how the statement was prepared, feedback and contact details, and the enforcement procedure. It was written for public bodies, not shops or banks, but its structure is sound, and borrowing it is the easiest way to cover everything Annex V asks for.&lt;/p&gt;

&lt;h2&gt;The sections a usable statement has&lt;/h2&gt;

&lt;p&gt;Combine Annex V, the public sector model and the &lt;a href="https://www.w3.org/WAI/planning/statements/" rel="noopener noreferrer"&gt;W3C WAI guidance on accessibility statements&lt;/a&gt; and you get a structure that works for a store, a booking site or a SaaS app:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SectionWhat goes in itWhy


ScopeWhich domains, apps and flows the statement coversA statement that covers "the website" is vague when checkout runs on a third party domain
About the serviceA short plain description of what the service does and how a customer uses itAnnex V points 1(a) and 1(b)
Standard appliedThe standard and level you test against, for example WCAG 2.2 Level AAAnnex V point 1(c) and point 2
Conformance statusYour honest assessment, in one sentenceThe part people overclaim most
Known limitationsEach open failure in plain words, with the criterion and a workaroundUsers need the workaround more than the criterion number
How we test and keep it accessibleTesting methods, dates, and the process that catches regressionsAnnex V point 3
Feedback and contactAt least two ways to reach a person, and a response timeThe route users actually take
EscalationThe national authority responsible for checking servicesTells users where to go if you do not answer
DatesWhen the statement was prepared and last reviewedShows whether anyone still maintains it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Link the statement from the footer of every page and from the terms and conditions, since Annex V names that document. The page itself has to be accessible: a statement published as a scanned PDF fails the very requirement it describes. If you do provide a PDF, run it through the &lt;a href="https://getaccessguard.com/pdf-accessibility-checker" rel="noopener noreferrer"&gt;PDF accessibility checker&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;W3C publishes a free &lt;a href="https://www.w3.org/WAI/planning/statements/generator/" rel="noopener noreferrer"&gt;accessibility statement generator&lt;/a&gt;. It makes a good first draft; add the Annex V service description and the monitoring section yourself.&lt;/p&gt;

&lt;h2&gt;How to state conformance without overclaiming&lt;/h2&gt;

&lt;p&gt;The most common mistake is one confident sentence: "This website is fully compliant with WCAG 2.2 AA." It is almost never supported by the testing behind it. Full conformance means every page and every criterion passes, and an automated scan cannot tell you that. Automated tools catch many failures, but a large share of WCAG criteria need a person to judge, such as whether alt text is meaningful or whether focus order makes sense. The &lt;a href="https://getaccessguard.com/posts/how-to-audit-your-website-for-accessibility" rel="noopener noreferrer"&gt;accessibility audit checklist&lt;/a&gt; separates what tools catch from what needs a person.&lt;/p&gt;

&lt;p&gt;Use the three levels from the public sector model, because they map honestly onto real sites:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Fully conforms:&lt;/strong&gt; only after a full audit, automated and manual, found no open failures in scope.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Partially conforms:&lt;/strong&gt; the right answer for most sites. Some content does not yet meet the standard, and you list it.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Does not conform:&lt;/strong&gt; most of the site fails, or you have not tested it. Say so and give a date for the work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also name what you tested and how. "Tested on 14 October 2026 with automated checks on 40 key pages and a manual keyboard and screen reader pass of checkout" is a claim someone can check. "Regularly audited by experts" is not.&lt;/p&gt;

&lt;h3&gt;A worked example with open failures&lt;/h3&gt;

&lt;p&gt;Here is honest wording for a fictional store with three known problems. Adapt the names, dates and findings to your own audit; do not copy the findings.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: an overclaim the audit does not support --&amp;gt;
&amp;lt;h2&amp;gt;Conformance status&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;Our website is 100% accessible and fully compliant
   with the European Accessibility Act.&amp;lt;/p&amp;gt;

&amp;lt;!-- Fixed: a status the audit supports, with the gaps listed --&amp;gt;
&amp;lt;h2&amp;gt;Conformance status&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;shop.example.com partially conforms to WCAG 2.2 Level AA.
   Partially conforms means some content does not yet meet
   the standard. The known problems are listed below.&amp;lt;/p&amp;gt;

&amp;lt;h2&amp;gt;Known limitations&amp;lt;/h2&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;The size guide opens in a pop-up that keyboard users
      cannot close without reloading the page (WCAG 2.1.2).
      The same table is on each product page under "Sizes".
      Planned fix: December 2026.&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;Some older product photos have no text description
      (WCAG 1.1.1). We are adding descriptions to the
      remaining photos, newest products first.&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;The delivery date picker cannot be used with a screen
      reader (WCAG 4.1.2). You can type the date instead,
      or choose a date with our support team.&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each item says what fails in plain words, gives the criterion, offers a workaround and says when it gets fixed. If you cannot commit to a date, say what you are doing instead. A dated promise you miss makes the statement untrue.&lt;/p&gt;

&lt;p&gt;Avoid writing "compliant with the European Accessibility Act" at all. The law is met or not met by the service, and that is decided by the authorities responsible for checking services, not by your own page. Describe the standard you test against and the results, and let that speak.&lt;/p&gt;

&lt;h2&gt;The feedback and escalation route&lt;/h2&gt;

&lt;p&gt;The second common mistake is a statement with no way to report a problem, or one that sends people to a contact form that is itself inaccessible. A usable feedback section has:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;an email address that reaches a person, not only a form;&lt;/li&gt;
  &lt;li&gt;a phone number, which also covers the "oral format" in Article 13(2) for people who cannot read the page;&lt;/li&gt;
  &lt;li&gt;a stated response time, such as "we reply within five working days";&lt;/li&gt;
  &lt;li&gt;an offer to provide information in another format on request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then name where people can escalate. Each member state designates authorities responsible for checking that services comply, and a user who gets no answer from you should be able to find them from your page. Look up the authority in the transposing law for each country you sell in and link it by name. Check the form you send people to with the &lt;a href="https://getaccessguard.com/checks/form-labels" rel="noopener noreferrer"&gt;form label checker&lt;/a&gt; and the &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; before you publish.&lt;/p&gt;

&lt;p&gt;Decide who reads those messages. An unmonitored feedback address turns a fixable bug into a complaint to an authority.&lt;/p&gt;

&lt;h2&gt;Keeping the statement true after the next release&lt;/h2&gt;

&lt;p&gt;Article 13(3) requires procedures so the service "remains in conformity" as it changes, and it names changes to the service, to the requirements and to the harmonised standards. A statement is accurate for the build you tested. Every release, theme update and new plugin can make it false.&lt;/p&gt;

&lt;p&gt;Standards are moving too. ETSI published EN 301 549 V4.1.1 in September 2026, and its web clauses now point at WCAG 2.2. As the &lt;a href="https://getaccessguard.com/posts/en-301-549-v4-1-1-what-changed-for-eu-client-sites" rel="noopener noreferrer"&gt;EN 301 549 v4.1.1 guide&lt;/a&gt; explains, no version is cited in the Official Journal under the EAA yet. If a statement names WCAG 2.1 or EN 301 549 V3.2.1, review whether that is still the standard you test against.&lt;/p&gt;

&lt;p&gt;Build the review into the work you already do:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;On every release:&lt;/strong&gt; rescan the pages the release touched and check whether any new failure belongs in "Known limitations".&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;When a limitation is fixed:&lt;/strong&gt; remove it from the statement in the same deploy, so the list stays honest in both directions.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;At least once a year:&lt;/strong&gt; repeat the manual pass, update the "last reviewed" date, and reread the national law for changes. The public sector model asks for a yearly review too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automated monitoring helps with the first item. AccessGuard, for example, rescans the pages you add on a daily, weekly or monthly schedule and alerts you to new failures, which tells you when the statement needs a new line. It runs automated WCAG 2.2 AA checks, so it cannot confirm conformance on its own; the manual pass still decides what the statement says.&lt;/p&gt;

&lt;h2&gt;Checklist before you publish&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Scope names every domain and app the statement covers, including third party checkout.&lt;/li&gt;
  &lt;li&gt;The service is described in plain words (Annex V point 1).&lt;/li&gt;
  &lt;li&gt;The standard and level are named, and the status matches the audit: usually "partially conforms".&lt;/li&gt;
  &lt;li&gt;Every known failure is listed in plain words, with the criterion, a workaround and a plan.&lt;/li&gt;
  &lt;li&gt;The testing method and date are stated, and so is the process that catches regressions (Annex V point 3).&lt;/li&gt;
  &lt;li&gt;Email and phone reach a person, with a response time.&lt;/li&gt;
  &lt;li&gt;The national authority for escalation is named and linked for each country.&lt;/li&gt;
  &lt;li&gt;The page is linked from every footer and from the terms and conditions, and the page itself passes a scan.&lt;/li&gt;
  &lt;li&gt;Someone owns the review on each release and once a year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you write the limitations section, find out what is actually open. &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;Run a free accessibility scan&lt;/a&gt; on the pages that matter most, then do the manual pass for what a scan cannot judge. The statement is only as honest as the testing behind it.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>wcag</category>
    </item>
    <item>
      <title>Elementor Accessibility: What Audits Find and How to Fix It</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:20:01 +0000</pubDate>
      <link>https://dev.to/chille87/elementor-accessibility-what-audits-find-and-how-to-fix-it-4je5</link>
      <guid>https://dev.to/chille87/elementor-accessibility-what-audits-find-and-how-to-fix-it-4je5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/elementor-accessibility-what-audits-find-and-how-to-fix-it" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;An Elementor site looks finished long before its markup is. Somebody dragged in a Heading widget, picked a size that looked right, linked a card, dropped in an icon for Instagram, and published. Every one of those choices writes HTML, and most of the accessibility failures on an Elementor site come from widget settings, not from code anybody typed.&lt;/p&gt;

&lt;p&gt;That is good news if you have just inherited one, from a client, a former freelancer or your own past self. Most fixes are a setting in the panel, not a rebuild. The catch is where those settings live: the worst failures sit in Theme Builder templates, and fixing pages one at a time never reaches them. This guide covers where Elementor sites fail, the fix for each, and an order to work in.&lt;/p&gt;

&lt;h2&gt;Where Elementor sites fail&lt;/h2&gt;

&lt;p&gt;Run an automated scan on a typical Elementor page and the findings cluster in a few places:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Headings&lt;/strong&gt; chosen for their size, so the outline skips levels or has three H1s.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Links with no name&lt;/strong&gt;: icon widgets, social icons and image links with nothing a screen reader can read.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Nested links&lt;/strong&gt; inside cards, where the whole container is a link and the button inside it is another.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Menus, tabs and accordions&lt;/strong&gt; that work with a mouse and only partly with a keyboard.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Contrast&lt;/strong&gt; from global colors that were picked in a design file and never checked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first four are covered below. You can see which of them a page has in about a minute with a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility scan&lt;/a&gt;, which is a good way to start before you open the editor.&lt;/p&gt;

&lt;h2&gt;Heading tag versus heading size&lt;/h2&gt;

&lt;p&gt;Elementor's Heading widget has two separate controls that people treat as one. The HTML Tag setting offers "H1 to H6, Div, Span, or Paragraph", and the font size lives in Typography (&lt;a href="https://elementor.com/help/heading-widget/" rel="noopener noreferrer"&gt;Elementor Heading widget docs&lt;/a&gt;). The tag is what a screen reader announces and what people navigate by. The size is only how it looks.&lt;/p&gt;

&lt;p&gt;When an editor picks H2 because it is the right size for a footer tagline, or H5 for a card title because H3 looked too big, the outline stops describing the page. That fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html" rel="noopener noreferrer"&gt;1.3.1 Info and Relationships&lt;/a&gt;: the visual structure says one thing and the markup says another.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: tags chosen by size --&amp;gt;
&amp;lt;h2 class="elementor-heading-title"&amp;gt;Trusted by local businesses&amp;lt;/h2&amp;gt; &amp;lt;!-- a tagline, not a section --&amp;gt;
&amp;lt;h5 class="elementor-heading-title"&amp;gt;Our services&amp;lt;/h5&amp;gt;              &amp;lt;!-- the real section heading --&amp;gt;

&amp;lt;!-- Fixed: tags follow the outline, Typography handles the size --&amp;gt;
&amp;lt;p class="elementor-heading-title"&amp;gt;Trusted by local businesses&amp;lt;/p&amp;gt;
&amp;lt;h2 class="elementor-heading-title"&amp;gt;Our services&amp;lt;/h2&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The fix is in the widget panel: set HTML Tag to the level the content needs, then set the size in Typography so the design does not change. Text that should look like a heading but is not one (a tagline, a big number, a quote) gets Paragraph or Div. Each page needs one H1, usually the page title, and levels should step down in order.&lt;/p&gt;

&lt;p&gt;Elementor's own Accessibility Assistant flags exactly these cases: a missing H1, more than one H1 and a heading that skips a level, with an option to "Keep the visual style but remove the semantic heading role" (&lt;a href="https://elementor.com/help/fixing-heading-accessibility-issues-with-ally-assistant/" rel="noopener noreferrer"&gt;Elementor help&lt;/a&gt;). Our &lt;a href="https://getaccessguard.com/checks/headings-and-landmarks" rel="noopener noreferrer"&gt;headings and landmarks checker&lt;/a&gt; shows the outline of any page, and the guide to &lt;a href="https://getaccessguard.com/posts/heading-structure-accessibility-h1s-skipped-levels-landmarks" rel="noopener noreferrer"&gt;heading structure&lt;/a&gt; covers the rules in detail.&lt;/p&gt;

&lt;h2&gt;Icon-only buttons and nested links inside cards&lt;/h2&gt;

&lt;h3&gt;Links and buttons with no name&lt;/h3&gt;

&lt;p&gt;An Icon widget or Social Icons widget with a link and no text produces a link that a screen reader announces as "link" and nothing else. The same happens with an image linked to a page when the image has no alt text. Both fail &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html" rel="noopener noreferrer"&gt;2.4.4 Link Purpose (In Context)&lt;/a&gt; and &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html" rel="noopener noreferrer"&gt;4.1.2 Name, Role, Value&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: an icon link with no accessible name --&amp;gt;
&amp;lt;a class="elementor-icon" href="https://instagram.com/example"&amp;gt;
  &amp;lt;svg aria-hidden="true"&amp;gt;...&amp;lt;/svg&amp;gt;
&amp;lt;/a&amp;gt;

&amp;lt;!-- Fixed: the link says where it goes --&amp;gt;
&amp;lt;a class="elementor-icon" href="https://instagram.com/example"
   aria-label="Example Bakery on Instagram"&amp;gt;
  &amp;lt;svg aria-hidden="true"&amp;gt;...&amp;lt;/svg&amp;gt;
&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You do not need code for this. Widgets with a Link field have a gear icon that opens Custom Attributes, which take a &lt;code&gt;key|value&lt;/code&gt; format (&lt;a href="https://elementor.com/help/custom-link-attributes/" rel="noopener noreferrer"&gt;Elementor custom link attributes&lt;/a&gt;). Enter &lt;code&gt;aria-label|Example Bakery on Instagram&lt;/code&gt; and the attribute lands on the link. For linked images, write alt text in the media library that describes the destination. Our &lt;a href="https://getaccessguard.com/checks/link-text" rel="noopener noreferrer"&gt;link text checker&lt;/a&gt; lists what a link name has to do to count.&lt;/p&gt;

&lt;h3&gt;The card that is a link with a link inside it&lt;/h3&gt;

&lt;p&gt;Elementor lets you make a whole container clickable: in Additional Options, set HTML Tag to "a (link)" (&lt;a href="https://elementor.com/help/making-a-container-clickable/" rel="noopener noreferrer"&gt;Elementor docs&lt;/a&gt;). It is the quickest way to build a clickable card, and it is where nested links come from. Elementor's own warning is plain: "When linking a container, the child elements within the container should NOT be linked themselves. Nesting &amp;lt;a&amp;gt; tags will cause issues."&lt;/p&gt;

&lt;p&gt;The HTML standard does not allow an &lt;code&gt;a&lt;/code&gt; element inside another &lt;code&gt;a&lt;/code&gt;, or any interactive element such as a button inside a link (&lt;a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element" rel="noopener noreferrer"&gt;HTML standard, the a element&lt;/a&gt;). Browsers repair the invalid markup in different ways, so a screen reader may hear the whole card as one long link, and a keyboard user may tab to two stops that go to the same place.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: a linked container with a linked button inside --&amp;gt;
&amp;lt;a class="e-con" href="/services/catering"&amp;gt;
  &amp;lt;h3&amp;gt;Catering&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;Breakfast and lunch for offices of 10 to 200.&amp;lt;/p&amp;gt;
  &amp;lt;a class="elementor-button" href="/services/catering"&amp;gt;Read more&amp;lt;/a&amp;gt;
&amp;lt;/a&amp;gt;

&amp;lt;!-- Fixed: one link, named by the heading --&amp;gt;
&amp;lt;div class="e-con"&amp;gt;
  &amp;lt;h3&amp;gt;&amp;lt;a href="/services/catering"&amp;gt;Catering&amp;lt;/a&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;Breakfast and lunch for offices of 10 to 200.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In Elementor, the fix is one of two things. Either link the container and remove the link from every child (the button can stay as a visual cue), or leave the container as a div and link the heading. The second is easier to understand with a screen reader, because the link name is the card title instead of every word in the card. Elementor's docs say version 3 of the editor will not stop you from nesting links, so check every card template, not only the ones built recently (&lt;a href="https://elementor.com/help/nested-links/" rel="noopener noreferrer"&gt;Elementor nested links&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;Menus, tabs and accordions&lt;/h2&gt;

&lt;p&gt;Interactive widgets are where a site can pass an automated scan and still fail a keyboard user, so these need a hand test. Unplug the mouse, press Tab from the top of the page, and check that every menu item, tab and accordion panel can be reached, opened and closed, with a visible focus ring the whole way (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html" rel="noopener noreferrer"&gt;2.1.1 Keyboard&lt;/a&gt;, &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html" rel="noopener noreferrer"&gt;2.4.7 Focus Visible&lt;/a&gt;). Our guide to &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;testing keyboard accessibility by hand&lt;/a&gt; walks the full pass.&lt;/p&gt;

&lt;h3&gt;Menus&lt;/h3&gt;

&lt;p&gt;Elementor has improved its menus over several releases, so the version installed matters. Version 3.13 made the mobile menu toggle work with Enter and Space as well as a click (&lt;a href="https://developers.elementor.com/elementor-3-13-developers-update/" rel="noopener noreferrer"&gt;Elementor 3.13 notes&lt;/a&gt;). Version 3.16 gave the Menu widget a &lt;code&gt;menubar&lt;/code&gt; role, &lt;code&gt;menuitem&lt;/code&gt; roles, arrow key navigation and Escape to close, based on the ARIA Authoring Practices (&lt;a href="https://developers.elementor.com/elementor-3-16-developers-update/" rel="noopener noreferrer"&gt;Elementor 3.16 notes&lt;/a&gt;). Version 3.23 added a Menu Name control for labelling each menu (&lt;a href="https://developers.elementor.com/elementor-3-23-developers-update/" rel="noopener noreferrer"&gt;Elementor 3.23 notes&lt;/a&gt;). An inherited site that has not been updated in a while may have none of this.&lt;/p&gt;

&lt;p&gt;Two checks after updating. First, a &lt;code&gt;menubar&lt;/code&gt; role promises application-style behaviour: arrow keys between items, Enter or Space to open a submenu and Escape to close it. Test that all of it works, because the W3C's own navigation example notes that "Typical site navigation does not need all the keyboard interactions specified by the menu and menubar pattern" and uses simpler disclosure buttons instead (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/" rel="noopener noreferrer"&gt;W3C disclosure navigation example&lt;/a&gt;). Second, when a page has more than one menu (header, footer, a sidebar), give each a distinct Menu Name so screen reader users can tell them apart. The &lt;a href="https://getaccessguard.com/posts/accessible-navigation-menus-and-dropdowns-that-work" rel="noopener noreferrer"&gt;navigation menus guide&lt;/a&gt; covers what good looks like.&lt;/p&gt;

&lt;h3&gt;Tabs and accordions&lt;/h3&gt;

&lt;p&gt;Tabs and accordions look alike and behave differently. An accordion header is a button that reports whether its panel is open (&lt;code&gt;aria-expanded&lt;/code&gt;), and each one is a Tab stop (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/accordion/" rel="noopener noreferrer"&gt;W3C accordion pattern&lt;/a&gt;). A tab list is one Tab stop, with arrow keys moving between tabs and the selected tab marked with &lt;code&gt;aria-selected&lt;/code&gt; (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/" rel="noopener noreferrer"&gt;W3C tabs pattern&lt;/a&gt;). Open the page with your browser's accessibility inspector and check that the Elementor widget exposes those states, then test the keys. If an FAQ was built from Toggle or Accordion widgets with headings inside the panels, check the heading levels there too. The guide to &lt;a href="https://getaccessguard.com/posts/accessible-accordions-and-tabs-two-patterns-everyone-confuses" rel="noopener noreferrer"&gt;accordions and tabs&lt;/a&gt; has the markup for both.&lt;/p&gt;

&lt;h2&gt;What the accessibility widget does not fix&lt;/h2&gt;

&lt;p&gt;Many Elementor sites also run Elementor's own accessibility plugin, now listed on WordPress.org as Web Accessibility (formerly Ally). On 26 October 2026 it showed 500,000+ active installations and a rating of 2.9 out of 5 from 162 ratings (&lt;a href="https://wordpress.org/plugins/pojo-accessibility/" rel="noopener noreferrer"&gt;WordPress.org plugin page&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It does two different things, and it helps to keep them apart. The Accessibility Assistant scans pages and guides you through fixes, such as the heading fixes above, and those fixes change the page. The usability widget gives visitors a floating menu to resize text, change contrast and similar adjustments. That changes how the page looks for one visitor; it does not add a missing label, name an icon link or untangle a nested link.&lt;/p&gt;

&lt;p&gt;The plugin is honest about this. Its description says it "is not a substitute for a thorough accessibility audit" and "is NOT intended to completely make your website legally compliant." The same is true of every automated tool, ours included: automated checks find many problems, but they cannot confirm a site conforms to WCAG. If a client believes the widget took care of accessibility, our piece on &lt;a href="https://getaccessguard.com/posts/accessibility-overlays-lawsuit-risk" rel="noopener noreferrer"&gt;accessibility overlays&lt;/a&gt; explains the difference in plain terms.&lt;/p&gt;

&lt;h2&gt;A fix order for an inherited site&lt;/h2&gt;

&lt;p&gt;Work from the templates that appear everywhere down to individual pages, so each fix reaches as many pages as possible.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
&lt;strong&gt;Update Elementor and the theme&lt;/strong&gt; on a staging copy, so you are fixing the current widgets and not ones that have since changed.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Scan a sample of pages&lt;/strong&gt;: the home page, one of each template type and the pages with forms. Save the results so you can show what changed.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Fix the Theme Builder header and footer.&lt;/strong&gt; They appear on every page, so the menu, logo link and social icons fixed here are fixed site wide.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Fix global colors and typography&lt;/strong&gt; in Site Settings, checking each text and background pair for contrast.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Fix single, archive and loop item templates&lt;/strong&gt;: card links, heading levels in post titles, image alt text in featured images.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Fix pages one by one&lt;/strong&gt;: heading tags, icon links, tabs and accordions, forms.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Test by hand&lt;/strong&gt; with the keyboard and a screen reader, then rescan the same pages and compare.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Next steps&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; on the home page and one inner page to see which of these failures the site has.&lt;/li&gt;
  &lt;li&gt;Check the outline of each template with the &lt;a href="https://getaccessguard.com/checks/headings-and-landmarks" rel="noopener noreferrer"&gt;headings and landmarks checker&lt;/a&gt; and every icon link with the &lt;a href="https://getaccessguard.com/checks/link-text" rel="noopener noreferrer"&gt;link text checker&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Search your card templates for linked containers with linked children.&lt;/li&gt;
  &lt;li&gt;Keyboard test every menu, tab set and accordion after updating Elementor.&lt;/li&gt;
  &lt;li&gt;For the rest of the WordPress stack (form plugins, theme CSS, media library), see &lt;a href="https://getaccessguard.com/posts/wordpress-accessibility-issues-and-fixes-for-agencies" rel="noopener noreferrer"&gt;WordPress accessibility issues and fixes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you explain this to a client or a manager, the short version is: most of it is settings, not a rebuild, and the templates come first because one fix there reaches every page.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>WCAG 2.2 vs 2.1: What Changed and What to Re-Test</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:14:05 +0000</pubDate>
      <link>https://dev.to/chille87/wcag-22-vs-21-what-changed-and-what-to-re-test-1gph</link>
      <guid>https://dev.to/chille87/wcag-22-vs-21-what-changed-and-what-to-re-test-1gph</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/wcag-2-2-vs-2-1-what-changed" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;If your site was built or audited against WCAG 2.1 and someone now asks for WCAG 2.2, the good news is that the gap is small and well defined. WCAG 2.2 keeps every WCAG 2.1 success criterion except one, adds nine new ones, and changes nothing about color contrast. This guide covers only the difference: what was added, what was removed, which of it applies at Level AA, and what a team that already meets 2.1 has to re-test.&lt;/p&gt;

&lt;p&gt;Every level and quotation below comes from the &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;WCAG 2.2 Recommendation&lt;/a&gt; (current edition dated 12 December 2024) and its &lt;a href="https://www.w3.org/TR/WCAG22/#comparison-with-wcag-2-1" rel="noopener noreferrer"&gt;Comparison with WCAG 2.1&lt;/a&gt; section. If you want the full list of Level A and AA criteria instead, use our &lt;a href="https://getaccessguard.com/posts/wcag-2-2-checklist-level-a-and-aa" rel="noopener noreferrer"&gt;WCAG 2.2 checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;The short answer&lt;/h2&gt;

&lt;p&gt;The W3C published WCAG 2.2 as a Recommendation on 5 October 2023, according to its own &lt;a href="https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/" rel="noopener noreferrer"&gt;What's new in WCAG 2.2&lt;/a&gt; page. The Recommendation describes the relationship in one sentence: "WCAG 2.2 builds on and is backwards compatible with WCAG 2.1, meaning web pages that conform to WCAG 2.2 are at least as accessible as pages that conform to WCAG 2.1."&lt;/p&gt;

&lt;p&gt;In practice that means three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nine criteria were added.&lt;/strong&gt; Six of them are Level A or AA, so they count for an AA target. Three are AAA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One criterion was removed:&lt;/strong&gt; 4.1.1 Parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else is the same.&lt;/strong&gt; The W3C says the 2.0 and 2.1 success criteria "are essentially the same in 2.2, with one exception", and that exception is Parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a site that meets WCAG 2.1 AA is not starting over. It needs to pass six additional checks.&lt;/p&gt;

&lt;h2&gt;Did color contrast change in WCAG 2.2?&lt;/h2&gt;

&lt;p&gt;No. This is the question people search for most often, so here is the plain answer. The two contrast criteria at Level AA are the same in both versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.4.3 Contrast (Minimum), AA:&lt;/strong&gt; text needs a contrast ratio of at least 4.5:1, or 3:1 for large text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.4.11 Non-text Contrast, AA:&lt;/strong&gt; the visual parts needed to identify controls and their states, and the parts of graphics needed to understand them, need at least 3:1 against adjacent colors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both were already in WCAG 2.1 (1.4.11 was added in 2.1) and neither changed in 2.2. If your text and controls passed contrast under 2.1, they pass under 2.2. You can confirm a page with our &lt;a href="https://getaccessguard.com/checks/color-contrast" rel="noopener noreferrer"&gt;color contrast checker&lt;/a&gt;, and our &lt;a href="https://getaccessguard.com/posts/color-contrast-requirements-wcag-ada" rel="noopener noreferrer"&gt;guide to contrast requirements&lt;/a&gt; explains the ratios in detail.&lt;/p&gt;

&lt;p&gt;The new criterion that touches contrast is &lt;strong&gt;2.4.13 Focus Appearance&lt;/strong&gt;, and it is Level AAA. It asks that a visible focus indicator be at least as large as a 2 CSS pixel thick perimeter of the component and have a contrast ratio of at least 3:1 between the focused and unfocused states. Because it is AAA, it is not part of an AA target. It is still a good design rule for focus outlines, and it is the reason many teams now use a solid 2 pixel outline.&lt;/p&gt;

&lt;h2&gt;The nine new criteria and their levels&lt;/h2&gt;

&lt;p&gt;CriterionLevelWhat it asks, in plain words&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html" rel="noopener noreferrer"&gt;2.4.11 Focus Not Obscured (Minimum)&lt;/a&gt;AAA focused element is not entirely hidden by content the author added, such as a sticky header or cookie banner.&lt;br&gt;
2.4.12 Focus Not Obscured (Enhanced)AAANo part of the focused element is hidden.&lt;br&gt;
2.4.13 Focus AppearanceAAAThe focus indicator is big enough and contrasts 3:1 with its unfocused state.&lt;br&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html" rel="noopener noreferrer"&gt;2.5.7 Dragging Movements&lt;/a&gt;AAAnything done by dragging can also be done with a single pointer without dragging, unless dragging is essential.&lt;br&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;2.5.8 Target Size (Minimum)&lt;/a&gt;AAClick and tap targets are at least 24 by 24 CSS pixels, or have enough space around them, with listed exceptions.&lt;br&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/consistent-help.html" rel="noopener noreferrer"&gt;3.2.6 Consistent Help&lt;/a&gt;AHelp such as contact details or a chat link, when repeated across pages, appears in the same relative order.&lt;br&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html" rel="noopener noreferrer"&gt;3.3.7 Redundant Entry&lt;/a&gt;AInformation the user already entered in the same process is filled in or offered for selection, not typed again.&lt;br&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html" rel="noopener noreferrer"&gt;3.3.8 Accessible Authentication (Minimum)&lt;/a&gt;AALogging in does not require a cognitive function test, like remembering a password or solving a puzzle, unless an alternative or a helper is available.&lt;br&gt;
3.3.9 Accessible Authentication (Enhanced)AAAAs 3.3.8, but without the object recognition and personal content exceptions.&lt;/p&gt;

&lt;p&gt;For an AA target, the six that matter are 2.4.11, 2.5.7, 2.5.8, 3.2.6, 3.3.7 and 3.3.8. For a deeper explanation of each one with examples, see our &lt;a href="https://getaccessguard.com/posts/full-wcag-22-coverage-what-we-added-and-why-most-scanners-miss-it" rel="noopener noreferrer"&gt;breakdown of the nine WCAG 2.2 criteria&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;What was removed: 4.1.1 Parsing&lt;/h2&gt;

&lt;p&gt;WCAG 2.1 had 4.1.1 Parsing at Level A. It asked for complete start and end tags, correct nesting, no duplicate attributes and unique IDs. WCAG 2.2 marks it "Obsolete and removed", with this note: "Assistive technology no longer has any need to directly parse HTML. Consequently, these problems either no longer exist or are addressed by other criteria."&lt;/p&gt;

&lt;p&gt;The W3C also edited WCAG 2.1 itself. The &lt;a href="https://www.w3.org/TR/WCAG21/" rel="noopener noreferrer"&gt;current WCAG 2.1 Recommendation&lt;/a&gt; (dated 6 May 2025) adds a note that 4.1.1 "should be considered as always satisfied for any content using HTML or XML". So even under 2.1, an HTML validator error is no longer a Parsing failure.&lt;/p&gt;

&lt;p&gt;That does not make markup errors harmless. A duplicate &lt;code&gt;id&lt;/code&gt; can still break a real relationship, and then the failure belongs to another criterion, usually 1.3.1 Info and Relationships or 4.1.2 Name, Role, Value:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: two inputs share id="email", so the second label points at the wrong field --&amp;gt;
&amp;lt;label for="email"&amp;gt;Billing email&amp;lt;/label&amp;gt;
&amp;lt;input id="email" type="email" autocomplete="email"&amp;gt;
&amp;lt;label for="email"&amp;gt;Shipping email&amp;lt;/label&amp;gt;
&amp;lt;input id="email" type="email"&amp;gt;

&amp;lt;!-- Fixed: unique ids, so each label names its own field --&amp;gt;
&amp;lt;label for="billing-email"&amp;gt;Billing email&amp;lt;/label&amp;gt;
&amp;lt;input id="billing-email" type="email" autocomplete="email"&amp;gt;
&amp;lt;label for="shipping-email"&amp;gt;Shipping email&amp;lt;/label&amp;gt;
&amp;lt;input id="shipping-email" type="email"&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One more detail from the Recommendation: authors who are required by policy to conform to WCAG 2.0 or 2.1 "may need to continue to test and report 4.1.1". If a contract or procurement form lists 4.1.1, answer it, and explain the W3C note.&lt;/p&gt;

&lt;h2&gt;What a team that meets 2.1 has to re-test&lt;/h2&gt;

&lt;p&gt;Here is the upgrade work for a site that already meets WCAG 2.1 AA, ordered by where failures usually turn up.&lt;/p&gt;

&lt;h3&gt;Sticky headers, footers and banners (2.4.11)&lt;/h3&gt;

&lt;p&gt;Tab through every template with the cookie banner open and closed. If a focused link scrolls under a fixed header and disappears completely, it fails. The usual fix is one line of CSS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: focused items scroll underneath a 72px fixed header */
.site-header { position: fixed; top: 0; height: 72px; }

/* Fixed: the browser keeps focused and scrolled-to items clear of the header */
html { scroll-padding-top: 80px; }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; covers tab order and focus on a page, and &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;how to test keyboard accessibility by hand&lt;/a&gt; walks through the manual pass.&lt;/p&gt;

&lt;h3&gt;Small targets (2.5.8)&lt;/h3&gt;

&lt;p&gt;Icon buttons, pagination links, close buttons and social icons are the usual suspects. A target under 24 by 24 CSS pixels can still pass if the spacing exception applies, and links inside a sentence are exempt. Run the &lt;a href="https://getaccessguard.com/checks/target-size" rel="noopener noreferrer"&gt;target size checker&lt;/a&gt; on your busiest templates, and read &lt;a href="https://getaccessguard.com/posts/wcag-2-5-8-target-size-minimum-24px" rel="noopener noreferrer"&gt;our guide to the 24 pixel rule&lt;/a&gt; for the exceptions.&lt;/p&gt;

&lt;h3&gt;Drag interactions (2.5.7)&lt;/h3&gt;

&lt;p&gt;List every slider, sortable list, map, kanban board and file drop zone. Each needs a way to do the same thing with clicks or taps alone, such as up and down buttons on a sortable list or a "choose file" button next to the drop zone.&lt;/p&gt;

&lt;h3&gt;Login, sign-up and password reset (3.3.8)&lt;/h3&gt;

&lt;p&gt;Check that password fields allow paste and password managers (no &lt;code&gt;autocomplete="off"&lt;/code&gt; on them), that one-time codes can be pasted, and that any CAPTCHA has an alternative that does not rely on a puzzle. Our &lt;a href="https://getaccessguard.com/posts/accessible-forms-how-to-build-inputs-every-user-can-complete" rel="noopener noreferrer"&gt;guide to accessible forms&lt;/a&gt; covers the field markup.&lt;/p&gt;

&lt;h3&gt;Multi-step forms (3.3.7)&lt;/h3&gt;

&lt;p&gt;Walk through checkout, booking and application flows. If step three asks for an address the user typed in step one, offer a "same as billing" checkbox or fill it in.&lt;/p&gt;

&lt;h3&gt;Help links (3.2.6)&lt;/h3&gt;

&lt;p&gt;If the header or footer has a contact link, phone number or chat button on several pages, make sure it sits in the same order relative to the rest of the page on each one. Templates usually get this right; one-off landing pages are where it slips.&lt;/p&gt;

&lt;h2&gt;Which version should you target?&lt;/h2&gt;

&lt;p&gt;Some rules still name WCAG 2.1. In the United States, the Department of Justice rule for state and local governments under ADA Title II adopted WCAG 2.1 Level AA (&lt;a href="https://www.federalregister.gov/documents/2024/04/24/2024-07758/nondiscrimination-on-the-basis-of-disability-accessibility-of-web-information-and-services-of-state" rel="noopener noreferrer"&gt;Federal Register, 89 FR 31320&lt;/a&gt;); our post on &lt;a href="https://getaccessguard.com/posts/ada-title-ii-deadline-extension-2027" rel="noopener noreferrer"&gt;the Title II deadline extension&lt;/a&gt; has the dates. In Europe, the new EN 301 549 V4.1.1 points its web requirements at WCAG 2.2, as we explain in &lt;a href="https://getaccessguard.com/posts/en-301-549-v4-1-1-what-changed-for-eu-client-sites" rel="noopener noreferrer"&gt;what changed in EN 301 549 V4.1.1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Because 2.2 is backwards compatible, you do not have to choose. The W3C says "sites which conform to WCAG 2.2 also conform to WCAG 2.1", and its Working Group "recommends that sites adopt WCAG 2.2 as their new conformance target, even if formal obligations mention previous versions". Building to 2.2 AA covers a 2.1 AA requirement too.&lt;/p&gt;

&lt;h3&gt;What to tell a manager or a client&lt;/h3&gt;

&lt;p&gt;Something like this works: "WCAG 2.2 is the current version. It keeps everything from 2.1, drops one outdated rule, and adds six requirements at our level, mostly about focus, tap targets, dragging and logins. Contrast rules did not change. We will test those six on our main templates and fix what fails." That is accurate, short and does not overpromise.&lt;/p&gt;

&lt;p&gt;A note on tools: automated checks, ours included, can flag many of these problems, but they cannot confirm that a site conforms to any version of WCAG. Several of the new criteria, such as Consistent Help and Redundant Entry, depend on how pages and processes behave, so a person still has to look.&lt;/p&gt;

&lt;h2&gt;Upgrade checklist: WCAG 2.1 AA to 2.2 AA&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep your 2.1 results. Contrast, alt text, headings, labels and the rest carry over unchanged.&lt;/li&gt;
&lt;li&gt;Drop 4.1.1 Parsing from your 2.2 report, but keep fixing duplicate ids that break labels or ARIA references.&lt;/li&gt;
&lt;li&gt;Tab through each template with sticky elements visible (2.4.11).&lt;/li&gt;
&lt;li&gt;Measure small icons and links against 24 by 24 CSS pixels (2.5.8).&lt;/li&gt;
&lt;li&gt;Give every drag action a click or tap alternative (2.5.7).&lt;/li&gt;
&lt;li&gt;Allow paste and password managers on login and reset forms (3.3.8).&lt;/li&gt;
&lt;li&gt;Remove repeated questions in multi-step forms (3.3.7).&lt;/li&gt;
&lt;li&gt;Keep help links in the same order across pages (3.2.6).&lt;/li&gt;
&lt;li&gt;Update your accessibility statement and reports to say WCAG 2.2 AA.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Want a starting point? Run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility scan&lt;/a&gt; on your home page and one key template. AccessGuard runs automated WCAG 2.2 AA checks in a real browser, including target size and focus checks, and shows which failures need a person to confirm.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>wcag</category>
    </item>
    <item>
      <title>Button vs Link Accessibility: The Choice That Breaks Keyboards</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:16:40 +0000</pubDate>
      <link>https://dev.to/chille87/button-vs-link-accessibility-the-choice-that-breaks-keyboards-4gkl</link>
      <guid>https://dev.to/chille87/button-vs-link-accessibility-the-choice-that-breaks-keyboards-4gkl</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/button-vs-link-accessibility" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;A designer hands over a mockup with a blue "Add to cart" pill and an underlined "Show more" link. A developer builds both as whatever is quickest: an &lt;code&gt;&amp;lt;a href="#"&amp;gt;&lt;/code&gt; for one, a &lt;code&gt;&amp;lt;div onclick&amp;gt;&lt;/code&gt; for the other. The page looks right, and it passes a mouse test. Then a keyboard user tabs straight past the cart, and a screen reader announces "link" for something that never goes anywhere.&lt;/p&gt;

&lt;p&gt;Choosing between a button and a link is one of the cheapest accessibility decisions on a site, and one of the most common things an audit flags. This guide covers what each element promises, what breaks when you swap them, how to fix a clickable div, and how to explain the finding to a client or a manager.&lt;/p&gt;

&lt;h2&gt;What each element promises&lt;/h2&gt;

&lt;p&gt;The element you choose is a promise about two things: how it behaves with a keyboard, and what a screen reader says about it. The WAI-ARIA Authoring Practices put the difference plainly: "The types of actions performed by buttons are distinctly different from the function of a link" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/button/" rel="noopener noreferrer"&gt;W3C APG, button pattern&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Link (&lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;)Button (&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;What it doesGoes somewhere: another page, a section, a fileDoes something: submits, opens, toggles, adds, deletes&lt;br&gt;
Keys that activate itEnterEnter and Space&lt;br&gt;
Screen reader announces"Link""Button"&lt;br&gt;
Browser extrasOpen in new tab, copy address, history, shows in a screen reader's links listCan submit a form, can be disabled&lt;/p&gt;

&lt;p&gt;A simple test: if the address bar should change, or the user might want to open it in a new tab, it is a link. If it changes something on the current page, it is a button.&lt;/p&gt;

&lt;h2&gt;Enter, Space and what breaks when you swap them&lt;/h2&gt;

&lt;p&gt;The APG link pattern gives a link one key: Enter "executes the link and moves focus to the link target" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/link/" rel="noopener noreferrer"&gt;W3C APG, link pattern&lt;/a&gt;). The button pattern gives a button two: both Space and Enter "activates the button" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/button/" rel="noopener noreferrer"&gt;W3C APG, button pattern&lt;/a&gt;). Native HTML elements do this for you. Swap them, and the behaviour goes with the element, not the design.&lt;/p&gt;

&lt;h3&gt;A link used as a button&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: announced as a link, Space scrolls the page instead of opening the menu --&amp;gt;
&amp;lt;a href="#" onclick="openMenu(); return false;"&amp;gt;Menu&amp;lt;/a&amp;gt;

&amp;lt;!-- Fixed: a button that says what it controls --&amp;gt;
&amp;lt;button type="button" aria-expanded="false" aria-controls="site-menu"&amp;gt;Menu&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Problems with the broken version: a screen reader user hears "link, Menu" and expects to go somewhere. Pressing Space, which people use for buttons, scrolls the page. If the script fails, &lt;code&gt;href="#"&lt;/code&gt; jumps to the top. And "open in new tab" opens a copy of the same page.&lt;/p&gt;

&lt;h3&gt;A button used as a link&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: navigation hidden in a script, no address to open or share --&amp;gt;
&amp;lt;button type="button" onclick="location.href='/pricing'"&amp;gt;See pricing&amp;lt;/button&amp;gt;

&amp;lt;!-- Fixed: a real link, styled however the design needs --&amp;gt;
&amp;lt;a href="/pricing" class="button"&amp;gt;See pricing&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This one is less harmful for keyboards, because the button still works, but it drops out of the screen reader's list of links, cannot be opened in a new tab and is not a link search engines can follow: Google says it "can follow your links only if they use proper &amp;lt;a&amp;gt; HTML element with an href attribute" (&lt;a href="https://developers.google.com/search/docs/crawling-indexing/links-crawlable" rel="noopener noreferrer"&gt;Google Search Central&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;A link with no href&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; with no &lt;code&gt;href&lt;/code&gt; is not a link at all. The HTML standard says that without an &lt;code&gt;href&lt;/code&gt; "the element represents a placeholder link" (&lt;a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element" rel="noopener noreferrer"&gt;HTML Living Standard&lt;/a&gt;), and browsers do not put it in the tab order. Click handlers on it work with a mouse and nothing else.&lt;/p&gt;

&lt;h2&gt;Fixing a div with an onclick&lt;/h2&gt;

&lt;p&gt;The most common version of this failure is a &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;span&lt;/code&gt; with a click handler. It looks like a control and acts like one with a mouse, but a keyboard cannot reach it and a screen reader announces it as plain text. That fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html" rel="noopener noreferrer"&gt;WCAG 2.1.1 Keyboard&lt;/a&gt;, which requires that "all functionality of the content is operable through a keyboard interface", and usually &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html" rel="noopener noreferrer"&gt;WCAG 4.1.2 Name, Role, Value&lt;/a&gt;, because nothing tells assistive technology it is a control.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: no focus, no role, no keyboard activation --&amp;gt;
&amp;lt;div class="add-to-cart" onclick="addToCart(42)"&amp;gt;Add to cart&amp;lt;/div&amp;gt;

&amp;lt;!-- Fixed: a native button gets focus, Enter, Space and its role for free --&amp;gt;
&amp;lt;button type="button" class="add-to-cart" onclick="addToCart(42)"&amp;gt;Add to cart&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two details catch people out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;type="button"&lt;/code&gt;.&lt;/strong&gt; Inside a form, a button with no type is a submit button. MDN warns that a button not meant to submit should have &lt;code&gt;type="button"&lt;/code&gt;, "Otherwise, they will try to submit form data" (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button" rel="noopener noreferrer"&gt;MDN, the button element&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset the styles, not the element.&lt;/strong&gt; Developers often avoid &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; because of its default look. A few lines of CSS remove that:&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;/* Fixed: a button that looks like plain text, with focus still visible */
.button-reset {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.button-reset:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Why role="button" plus tabindex is the last resort&lt;/h3&gt;

&lt;p&gt;You can make a &lt;code&gt;div&lt;/code&gt; behave like a button with ARIA, but you have to rebuild everything the browser would have given you:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Works, but only with the script below. Use a real button if you can. --&amp;gt;
&amp;lt;div role="button" tabindex="0" class="add-to-cart"&amp;gt;Add to cart&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;// Every one of these lines is something &amp;lt;button&amp;gt; already does
el.addEventListener("click", addToCart);
el.addEventListener("keydown", (event) =&amp;gt; {
  if (event.key === "Enter") addToCart();
  if (event.key === " ") event.preventDefault(); // stop the page scrolling
});
el.addEventListener("keyup", (event) =&amp;gt; {
  if (event.key === " ") addToCart();
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And you still have no disabled state, no form submission, and a component that breaks the first time someone edits the markup and drops the script. The W3C's guidance on ARIA says the same as its first rule: if a native HTML element already has the semantics and behaviour you need, use it instead of adding a role to something else (&lt;a href="https://www.w3.org/TR/using-aria/#rule1" rel="noopener noreferrer"&gt;Using ARIA, first rule&lt;/a&gt;). Keep this pattern for markup you genuinely cannot change, such as a third-party widget.&lt;/p&gt;

&lt;h2&gt;When a link should look like a button&lt;/h2&gt;

&lt;p&gt;Design and semantics are separate decisions. A "Start free trial" call to action that goes to a signup page is a link, even if it is a big coloured pill. Give it a &lt;code&gt;.button&lt;/code&gt; class and keep the &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;. A "Show more" control that expands text on the same page is a button, even if it is styled as underlined text.&lt;/p&gt;

&lt;p&gt;The APG notes that a link-styled element acting as a button can take &lt;code&gt;role="button"&lt;/code&gt;, but says "a better solution is to adjust the visual design so it matches the function and ARIA role" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/button/" rel="noopener noreferrer"&gt;W3C APG, button pattern&lt;/a&gt;). In practice that means: pick the element by what it does, then style it, and keep a visible focus style on both.&lt;/p&gt;

&lt;p&gt;Whichever you choose, give it a name that makes sense on its own. An icon-only button needs an accessible name, and a link's text should say where it goes. Our guide to &lt;a href="https://getaccessguard.com/posts/aria-attributes-when-and-how-to-use-them" rel="noopener noreferrer"&gt;ARIA attributes&lt;/a&gt; covers when ARIA helps and when it gets in the way, and you can &lt;a href="https://getaccessguard.com/checks/link-text" rel="noopener noreferrer"&gt;check a page for links that do not say where they go&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;What an audit flags and how to explain it to a client&lt;/h2&gt;

&lt;p&gt;Automated tools find the clearest cases: elements with click handlers that are not controls, links with no &lt;code&gt;href&lt;/code&gt;, and controls with no accessible name. Our free &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; looks for clickable divs and spans that a keyboard cannot reach, along with removed focus styles. What a tool cannot judge is intent: whether a real &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; that runs a script should have been a button. That takes a person, so pair the scan with a &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;keyboard test by hand&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When you report it, skip the jargon. Something like this works for a manager or a client:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;"Some of the controls on the site, including the add to cart button and the menu toggle, only work with a mouse. People who use a keyboard or a screen reader cannot use them, and so cannot buy. The fix is to build them with the standard HTML button element, which takes a developer minutes per control and does not change how the site looks."&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;That framing is accurate and gives the person paying for the work a reason to prioritise it: the broken controls tend to sit on the pages that matter most, such as navigation, checkout and forms.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; on your home page, a product or service page and the checkout or contact form.&lt;/li&gt;
&lt;li&gt;Replace every &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;span&lt;/code&gt; with a click handler with a &lt;code&gt;&amp;lt;button type="button"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replace every &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; with no &lt;code&gt;href&lt;/code&gt;, or with &lt;code&gt;href="#"&lt;/code&gt; and a script, with a button.&lt;/li&gt;
&lt;li&gt;Replace every button that only changes the page address with an &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give icon-only buttons and links an accessible name.&lt;/li&gt;
&lt;li&gt;Tab through the page: every control should be reachable, show visible focus, and work with Enter (and Space, for buttons).&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;role="button"&lt;/code&gt; with &lt;code&gt;tabindex="0"&lt;/code&gt; only for markup you cannot change, with the keyboard script in place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the wider picture, our guide to &lt;a href="https://getaccessguard.com/posts/keyboard-navigation-building-websites-everyone-can-use" rel="noopener noreferrer"&gt;keyboard navigation&lt;/a&gt; covers focus order, skip links and keyboard traps.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>html</category>
    </item>
    <item>
      <title>Accessible Navigation Menus and Dropdowns That Work</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:15:21 +0000</pubDate>
      <link>https://dev.to/chille87/accessible-navigation-menus-and-dropdowns-that-work-2nam</link>
      <guid>https://dev.to/chille87/accessible-navigation-menus-and-dropdowns-that-work-2nam</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/accessible-navigation-menus-and-dropdowns-that-work" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;The main navigation is the one component every site has, and the one a keyboard user meets first. When it fails, they cannot reach anything else. It is also the component where most of the ARIA advice you find online makes things worse, because it starts from the word "menu".&lt;/p&gt;

&lt;p&gt;This guide covers the pattern W3C recommends for site navigation, the markup for it, what to do about hover submenus and mobile toggles, and how to explain the findings to whoever owns the site. It applies whether you write the markup yourself or are choosing a theme, a page builder block or a plugin.&lt;/p&gt;

&lt;h2&gt;Why role="menu" is usually the wrong choice for site navigation&lt;/h2&gt;

&lt;p&gt;In everyday speech a navigation bar is a "menu". In ARIA, &lt;code&gt;role="menu"&lt;/code&gt; and &lt;code&gt;role="menubar"&lt;/code&gt; mean something narrower: a widget that behaves like the menu bar at the top of a desktop application. The W3C ARIA Authoring Practices Guide (APG) defines a menu as "a widget that offers a list of choices to the user, such as a set of actions or functions" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" rel="noopener noreferrer"&gt;APG, Menu and Menubar Pattern&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Adding those roles is a promise. Screen readers announce a menu and expect the keyboard model that comes with it: the arrow keys move between items, only one item sits in the Tab sequence, and Tab leaves the whole menubar. Most navigation bars that carry &lt;code&gt;role="menubar"&lt;/code&gt; deliver none of that. They are a list of links with the roles pasted on, so the user is told one thing and gets another.&lt;/p&gt;

&lt;p&gt;The APG's own navigation menubar example opens with a warning: "The menubar pattern requires complex functionality that is unnecessary for typical site navigation that is styled to look like a menubar with expandable sections or fly outs. A pattern more suited for typical site navigation with expandable groups of links is the Disclosure Pattern" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/" rel="noopener noreferrer"&gt;APG, Navigation Menubar Example&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So the menubar is not forbidden. It is a lot of work to get right, and for a list of pages it buys nothing. If your navigation is a set of links, some of them grouped under section headings, use the disclosure pattern below.&lt;/p&gt;

&lt;p&gt;One line on a related guide: a custom select or autocomplete field is a combobox, a different widget with its own rules, covered in &lt;a href="https://getaccessguard.com/posts/how-to-build-accessible-custom-dropdowns-comboboxes-that-actually-work" rel="noopener noreferrer"&gt;how to build accessible custom dropdowns and comboboxes&lt;/a&gt;. This guide is about site navigation only.&lt;/p&gt;

&lt;h2&gt;The disclosure pattern: a button and a list of links&lt;/h2&gt;

&lt;p&gt;The APG's disclosure navigation example "does not use the menu role because it does not provide the complex functionality that assistive technologies expect in a widget that has the menu role. Typical site navigation does not need all the keyboard interactions specified by the menu and menubar pattern" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/" rel="noopener noreferrer"&gt;APG, Example Disclosure Navigation Menu&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The structure is plain HTML. A &lt;code&gt;nav&lt;/code&gt; landmark holds a list. A top-level item is either a link, or a &lt;code&gt;button&lt;/code&gt; that shows and hides a nested list of links. The button carries two attributes: &lt;code&gt;aria-expanded&lt;/code&gt; says whether its list is open, and &lt;code&gt;aria-controls&lt;/code&gt; points to the list.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: menu roles with no menu keyboard model, and a div as the trigger --&amp;gt;
&amp;lt;ul role="menubar"&amp;gt;
  &amp;lt;li role="menuitem"&amp;gt;
    &amp;lt;div class="nav-trigger"&amp;gt;Services&amp;lt;/div&amp;gt;
    &amp;lt;ul role="menu" class="submenu"&amp;gt;
      &amp;lt;li role="menuitem"&amp;gt;&amp;lt;a href="/services/audits"&amp;gt;Audits&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;!-- Fixed: a nav landmark, a real button, and links in a list --&amp;gt;
&amp;lt;nav aria-label="Main"&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;li&amp;gt;&amp;lt;a href="/"&amp;gt;Home&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
    &amp;lt;li&amp;gt;
      &amp;lt;button type="button" aria-expanded="false" aria-controls="nav-services"&amp;gt;Services&amp;lt;/button&amp;gt;
      &amp;lt;ul id="nav-services" hidden&amp;gt;
        &amp;lt;li&amp;gt;&amp;lt;a href="/services/audits"&amp;gt;Audits&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;&amp;lt;a href="/services/fixes"&amp;gt;Fixes&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/nav&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The script only has to flip two things when the button is pressed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Toggle the submenu and keep aria-expanded in step with what is on screen
document.querySelectorAll('nav button[aria-controls]').forEach((button) =&amp;gt; {
  const list = document.getElementById(button.getAttribute('aria-controls'));
  button.addEventListener('click', () =&amp;gt; {
    const open = button.getAttribute('aria-expanded') === 'true';
    button.setAttribute('aria-expanded', String(!open));
    list.hidden = open;
  });
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because it is a real &lt;code&gt;button&lt;/code&gt;, Enter and Space work and it is announced as a button with its state, which is what &lt;strong&gt;4.1.2 Name, Role, Value (Level A)&lt;/strong&gt; asks for: the name and role "can be programmatically determined", and for its state, "notification of changes to these items is available to user agents, including assistive technologies" (&lt;a href="https://www.w3.org/TR/WCAG22/#name-role-value" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;). Our &lt;a href="https://getaccessguard.com/posts/aria-attributes-when-and-how-to-use-them" rel="noopener noreferrer"&gt;guide to ARIA attributes&lt;/a&gt; explains why the native element beats a &lt;code&gt;div&lt;/code&gt; with roles added.&lt;/p&gt;

&lt;h3&gt;The keyboard behaviour to build&lt;/h3&gt;

&lt;p&gt;From the APG disclosure example, the required keys are few:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Tab and Shift + Tab&lt;/strong&gt; move between the top-level links and buttons, and through the links of an open list.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Enter or Space&lt;/strong&gt; on a button opens or closes its list.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Escape&lt;/strong&gt;, when a list is open, closes it and puts focus back on its button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Arrow keys, Home and End are marked optional in that example. Add them if you like, but Tab, Enter, Space and Escape are what &lt;strong&gt;2.1.1 Keyboard (Level A)&lt;/strong&gt; depends on: "All functionality of the content is operable through a keyboard interface" (&lt;a href="https://www.w3.org/TR/WCAG22/#keyboard" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;). The general rules for focus and tab order are in our guide to &lt;a href="https://getaccessguard.com/posts/keyboard-navigation-building-websites-everyone-can-use" rel="noopener noreferrer"&gt;keyboard navigation and focus management&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;Should the section name be a link too?&lt;/h3&gt;

&lt;p&gt;A common design has "Services" as both a page and a submenu trigger. One element cannot be both: a link goes somewhere on Enter, a button opens the list. Either make the top-level item a link and give the submenu a separate small button beside it (with an accessible name such as "Services submenu"), or keep the button and put an "All services" link as the first item in the list. The second is simpler and works the same on touch screens.&lt;/p&gt;

&lt;h2&gt;Hover submenus and what 1.4.13 asks for&lt;/h2&gt;

&lt;p&gt;Many sites open submenus on mouse hover. You can keep that, as long as keyboard users have the button and the hover behaviour passes &lt;strong&gt;1.4.13 Content on Hover or Focus (Level AA)&lt;/strong&gt;. W3C lists "sub-menus" among the content it covers (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html" rel="noopener noreferrer"&gt;Understanding SC 1.4.13&lt;/a&gt;). Content that appears on hover or focus must be:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Dismissible:&lt;/strong&gt; "A mechanism is available to dismiss the additional content without moving pointer hover or keyboard focus". In practice, Escape closes the open submenu.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Hoverable:&lt;/strong&gt; "the pointer can be moved over the additional content without the additional content disappearing". A gap between the top item and the submenu that closes it as the pointer crosses fails this.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Persistent:&lt;/strong&gt; it "remains visible until the hover or focus trigger is removed, the user dismisses it, or its information is no longer valid". No timer that hides it after two seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: a 12px gap closes the submenu before the pointer reaches it */
.nav li { position: relative; }
.nav .submenu { position: absolute; top: calc(100% + 12px); }

/* Fixed: the submenu touches its trigger; the gap is padding inside it */
.nav .submenu { position: absolute; top: 100%; padding-top: 12px; }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Never make hover the only way in. A submenu that opens on hover and has no button is unreachable by keyboard and on many touch screens, which is a 2.1.1 failure before 1.4.13 even comes up.&lt;/p&gt;

&lt;h2&gt;The mobile toggle&lt;/h2&gt;

&lt;p&gt;On small screens the whole navigation usually hides behind a single toggle. It is the same disclosure pattern one level up, and it fails in the same few ways.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: an icon in a div, no name, no state --&amp;gt;
&amp;lt;div class="hamburger" onclick="toggleNav()"&amp;gt;&amp;lt;svg&amp;gt;...&amp;lt;/svg&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;!-- Fixed: a named button that reports whether the navigation is open --&amp;gt;
&amp;lt;button type="button" class="nav-toggle" aria-expanded="false" aria-controls="site-nav"&amp;gt;
  &amp;lt;svg aria-hidden="true" focusable="false"&amp;gt;...&amp;lt;/svg&amp;gt;
  &amp;lt;span class="visually-hidden"&amp;gt;Menu&amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then check what happens around it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Focus order.&lt;/strong&gt; After the toggle opens the navigation, the next Tab should land on the first link inside it. If the panel sits at the end of the markup, focus wanders through the whole page first.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Escape.&lt;/strong&gt; It should close the panel and return focus to the toggle.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;A full-screen panel.&lt;/strong&gt; If it covers the page, the content underneath must not take focus while it is open, or keyboard users tab into links they cannot see. The HTML &lt;code&gt;inert&lt;/code&gt; attribute on the main content does this. A panel that covers the page like this behaves as a dialog, so the focus rules in our &lt;a href="https://getaccessguard.com/posts/accessible-modals-how-to-build-dialogs-that-dont-trap-confuse-or-exclude-users" rel="noopener noreferrer"&gt;guide to accessible modals&lt;/a&gt; apply.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;A sticky header.&lt;/strong&gt; When focus moves down the page, the sticky bar must not cover the focused element entirely. That is &lt;strong&gt;2.4.11 Focus Not Obscured (Minimum), Level AA&lt;/strong&gt; (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html" rel="noopener noreferrer"&gt;Understanding SC 2.4.11&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Marking the current page with aria-current&lt;/h2&gt;

&lt;p&gt;Sighted users see the current page highlighted in the navigation. Screen reader users need the same signal in the markup. ARIA's &lt;code&gt;aria-current&lt;/code&gt; "indicates the element that represents the current item within a container or set of related elements", and "Authors SHOULD only mark one element in a set of elements as current" (&lt;a href="https://www.w3.org/TR/wai-aria-1.2/#aria-current" rel="noopener noreferrer"&gt;WAI-ARIA 1.2&lt;/a&gt;).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: the highlight is only a class, so nothing is announced --&amp;gt;
&amp;lt;a href="/services/audits" class="active"&amp;gt;Audits&amp;lt;/a&amp;gt;

&amp;lt;!-- Fixed: announced as the current page, and styled from the same attribute --&amp;gt;
&amp;lt;a href="/services/audits" aria-current="page"&amp;gt;Audits&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Style it with &lt;code&gt;[aria-current="page"]&lt;/code&gt; rather than a separate class, so the visual highlight and the announced state can never disagree. Most CMSes and themes print an "active" class already; adding the attribute in the same template is usually one line.&lt;/p&gt;

&lt;h3&gt;More than one navigation on a page&lt;/h3&gt;

&lt;p&gt;Header, footer and breadcrumb navigation are all &lt;code&gt;nav&lt;/code&gt; landmarks. The APG says: "If a page includes more than one navigation landmark, each should have a unique label" (&lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/navigation.html" rel="noopener noreferrer"&gt;APG, Navigation Landmark&lt;/a&gt;). &lt;code&gt;aria-label="Main"&lt;/code&gt;, &lt;code&gt;aria-label="Footer"&lt;/code&gt; and &lt;code&gt;aria-label="Breadcrumb"&lt;/code&gt; are enough. Leave the word "navigation" out of the label, because screen readers already announce the landmark type.&lt;/p&gt;

&lt;p&gt;Link text matters in navigation too: icon-only social links and repeated "More" links are common findings. The &lt;a href="https://getaccessguard.com/checks/link-text" rel="noopener noreferrer"&gt;link text checker&lt;/a&gt; flags links with no name or vague text.&lt;/p&gt;

&lt;h2&gt;What a scan finds and what needs a person&lt;/h2&gt;

&lt;p&gt;An automated scan can find a lot of this in the rendered page: invalid ARIA such as menu roles missing their required children, buttons and links with no accessible name, and divs with click handlers that a keyboard cannot reach. Our &lt;a href="https://getaccessguard.com/checks/aria" rel="noopener noreferrer"&gt;ARIA checker&lt;/a&gt; and &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; look for those, and a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility audit&lt;/a&gt; runs them on any page.&lt;/p&gt;

&lt;p&gt;A scan cannot tell you whether the submenu stays open while the pointer crosses to it, whether Escape returns focus to the button, or whether the mobile panel lets focus escape underneath it. 1.4.13 in particular is a manual check. No automated tool can confirm a navigation conforms to WCAG. Our step by step &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;manual keyboard accessibility test&lt;/a&gt; covers the rest in about twenty minutes.&lt;/p&gt;

&lt;h3&gt;How to explain it to a client or manager&lt;/h3&gt;

&lt;p&gt;Keep it concrete: "Keyboard users cannot open the Services menu, so they cannot reach the six pages under it." That is a sentence a manager can act on. The fix for most navigation findings is small, a button instead of a div, two attributes and an Escape handler, and it usually lives in one template, so it fixes every page at once.&lt;/p&gt;

&lt;h2&gt;Your checklist&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;The navigation is a &lt;code&gt;nav&lt;/code&gt; with a list of links, and no &lt;code&gt;menu&lt;/code&gt;, &lt;code&gt;menubar&lt;/code&gt; or &lt;code&gt;menuitem&lt;/code&gt; roles unless you built the full menubar keyboard model.&lt;/li&gt;
  &lt;li&gt;Every submenu trigger is a &lt;code&gt;button&lt;/code&gt; with &lt;code&gt;aria-expanded&lt;/code&gt; that changes when it opens and closes.&lt;/li&gt;
  &lt;li&gt;Enter and Space open a submenu; Escape closes it and returns focus to its button.&lt;/li&gt;
  &lt;li&gt;Hover submenus also open from the button, stay open while the pointer moves onto them and have no auto-hide timer.&lt;/li&gt;
  &lt;li&gt;The mobile toggle is a named button with &lt;code&gt;aria-expanded&lt;/code&gt;, focus moves into the panel, and nothing behind a full-screen panel takes focus.&lt;/li&gt;
  &lt;li&gt;The sticky header never fully covers the focused element.&lt;/li&gt;
  &lt;li&gt;The current page link has &lt;code&gt;aria-current="page"&lt;/code&gt;, and the highlight is styled from it.&lt;/li&gt;
  &lt;li&gt;Each &lt;code&gt;nav&lt;/code&gt; on the page has a unique label.&lt;/li&gt;
  &lt;li&gt;Run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; for the countable problems, then test the navigation by keyboard, on a phone and with the pointer.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>html</category>
    </item>
    <item>
      <title>Agent Ready Website: What AI Browser Agents Need to Read</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:19:18 +0000</pubDate>
      <link>https://dev.to/chille87/agent-ready-website-what-ai-browser-agents-need-to-read-26g1</link>
      <guid>https://dev.to/chille87/agent-ready-website-what-ai-browser-agents-need-to-read-26g1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/agent-ready-website-what-ai-browser-agents-need" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;A browser agent is software that opens a web page and uses it on someone's behalf: it reads the page, picks a link, fills in a form, presses the button. If your site's buttons have no names, its form fields have no labels and its menus are built from bare &lt;code&gt;div&lt;/code&gt; elements, the agent gets stuck in the same places a screen reader user does. That is the whole idea behind an agent ready website, and it is good news, because the fix is work you can already measure against &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This guide explains what agents read, the five failures that stop them, the markup that fixes each one, and how to check your own site. You can run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility scan&lt;/a&gt; on any page while you read, and our &lt;a href="https://getaccessguard.com/ai-agent-ready" rel="noopener noreferrer"&gt;AI agent readiness page&lt;/a&gt; maps each need to the check that finds it.&lt;/p&gt;

&lt;h2&gt;How a browser agent reads your page&lt;/h2&gt;

&lt;p&gt;Agents differ, and they change often, but they read a page in some mix of three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The HTML&lt;/strong&gt; the browser has rendered: element types, attributes and text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The accessibility tree.&lt;/strong&gt; The browser builds this from the HTML for assistive technology. Each node has a role (button, link, textbox, heading), an accessible name and a state (checked, expanded, disabled). It is what a screen reader reads aloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots,&lt;/strong&gt; read by a vision model, with clicks sent as screen coordinates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of this is documented by the people who build them. Microsoft's &lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;Playwright MCP server&lt;/a&gt;, which lets an AI model drive a browser, says it "uses Playwright's accessibility tree, not pixel-based input". OpenAI's &lt;a href="https://help.openai.com/en/articles/12627856-publishers-and-developers-faq" rel="noopener noreferrer"&gt;publishers and developers FAQ&lt;/a&gt; says its Atlas browser uses ARIA, the same roles and labels screen readers rely on, to understand page structure and controls, and advises site owners to follow WAI-ARIA best practices for buttons, menus and forms. Anthropic's &lt;a href="https://platform.claude.com/docs/agents-and-tools/tool-use/computer-use-tool" rel="noopener noreferrer"&gt;computer use tool&lt;/a&gt; works the other way, from screenshots, with click coordinates "in screenshot pixels".&lt;/p&gt;

&lt;p&gt;So an agent that reads the tree needs the structure to be in the markup, and an agent that reads pixels needs what is on screen to be legible and big enough to hit. Both are WCAG topics.&lt;/p&gt;

&lt;h2&gt;Is your website AI ready? The five failures that stop agents&lt;/h2&gt;

&lt;p&gt;These are the problems that leave an agent with nothing to choose by. Each is also a WCAG failure that hurts people using assistive technology, which is why fixing them is not a bet on any one AI product.&lt;/p&gt;

&lt;h3&gt;1. Icon buttons with no accessible name&lt;/h3&gt;

&lt;p&gt;A button that shows only an icon has a role but no name. In the accessibility tree it reads as "button" and nothing else, so a cart button, a search button and a menu button look identical. This fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html" rel="noopener noreferrer"&gt;4.1.2 Name, Role, Value&lt;/a&gt; (Level A).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: the tree says "button" and nothing more --&amp;gt;
&amp;lt;button class="icon-cart"&amp;gt;
  &amp;lt;svg viewBox="0 0 24 24"&amp;gt;...&amp;lt;/svg&amp;gt;
&amp;lt;/button&amp;gt;

&amp;lt;!-- Fixed: the tree says "button, View cart" --&amp;gt;
&amp;lt;button class="icon-cart" aria-label="View cart"&amp;gt;
  &amp;lt;svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"&amp;gt;...&amp;lt;/svg&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our &lt;a href="https://getaccessguard.com/checks/aria" rel="noopener noreferrer"&gt;ARIA checker&lt;/a&gt; flags buttons and links with no name. The same rule applies to linked logos and image links, where the name comes from the alt text (see &lt;a href="https://getaccessguard.com/posts/alt-text-done-right-writing-image-descriptions-that-work" rel="noopener noreferrer"&gt;how to write alt text&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;2. Custom controls with no role&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;div&lt;/code&gt; with a click handler looks like a button and acts like one for a mouse. To the accessibility tree it is a generic container with some text in it, so an agent that looks for interactive elements may not find it, and a keyboard cannot reach it. That fails 4.1.2 and &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html" rel="noopener noreferrer"&gt;2.1.1 Keyboard&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: a generic element with a click handler --&amp;gt;
&amp;lt;div class="btn" onclick="addToCart()"&amp;gt;Add to cart&amp;lt;/div&amp;gt;

&amp;lt;!-- Fixed: a real button, focusable and announced as one --&amp;gt;
&amp;lt;button type="button" onclick="addToCart()"&amp;gt;Add to cart&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The same goes for custom dropdowns, tabs and toggles. If you build them yourself, they need the right role and state, as in &lt;code&gt;aria-expanded="true"&lt;/code&gt; on a menu button, so the tree says what they are and whether they are open. Our &lt;a href="https://getaccessguard.com/posts/aria-attributes-when-and-how-to-use-them" rel="noopener noreferrer"&gt;guide to ARIA attributes&lt;/a&gt; covers the patterns, and the &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard accessibility checker&lt;/a&gt; flags click handlers with no keyboard access.&lt;/p&gt;

&lt;h3&gt;3. Form fields with no programmatic label&lt;/h3&gt;

&lt;p&gt;A placeholder is not a label. It disappears when someone types, and a field labelled only by nearby text has no name in the accessibility tree. An agent filling a checkout form then has to guess which box is the postcode. This fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html" rel="noopener noreferrer"&gt;1.3.1 Info and Relationships&lt;/a&gt; and 4.1.2.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: no label, only a placeholder --&amp;gt;
&amp;lt;input type="text" name="zip" placeholder="ZIP"&amp;gt;

&amp;lt;!-- Fixed: a label tied to the field, and the purpose stated --&amp;gt;
&amp;lt;label for="zip"&amp;gt;ZIP or postal code&amp;lt;/label&amp;gt;
&amp;lt;input type="text" id="zip" name="zip" autocomplete="postal-code"&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;autocomplete&lt;/code&gt; value is a bonus: it states the field's purpose in a fixed vocabulary that browsers and password managers already understand (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html" rel="noopener noreferrer"&gt;1.3.5 Identify Input Purpose&lt;/a&gt;). The &lt;a href="https://getaccessguard.com/checks/form-labels" rel="noopener noreferrer"&gt;form label checker&lt;/a&gt; finds unlabelled fields, and our &lt;a href="https://getaccessguard.com/posts/accessible-forms-how-to-build-inputs-every-user-can-complete" rel="noopener noreferrer"&gt;accessible forms guide&lt;/a&gt; covers errors and grouping.&lt;/p&gt;

&lt;h3&gt;4. Names that do not match what is on screen&lt;/h3&gt;

&lt;p&gt;An agent working from a screenshot sees the word on the button. An agent working from the tree sees the accessible name. When they disagree, the two kinds of agent are looking at different pages, and so are people who use voice control. WCAG has a criterion for exactly this, &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html" rel="noopener noreferrer"&gt;2.5.3 Label in Name&lt;/a&gt; (Level A): the accessible name has to contain the visible text.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: shows "Buy now", the tree says "Proceed" --&amp;gt;
&amp;lt;button aria-label="Proceed"&amp;gt;Buy now&amp;lt;/button&amp;gt;

&amp;lt;!-- Fixed: let the visible text be the name --&amp;gt;
&amp;lt;button&amp;gt;Buy now&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Most of the time the fix is to delete an &lt;code&gt;aria-label&lt;/code&gt; that was never needed. Visible text already names a button.&lt;/p&gt;

&lt;h3&gt;5. Links and structure that all look the same&lt;/h3&gt;

&lt;p&gt;Ten links called "Read more" are ten identical entries in the accessibility tree. A page with no headings and no &lt;code&gt;main&lt;/code&gt; landmark gives an agent no way to skip the navigation and find the content. Link text that says where it goes (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html" rel="noopener noreferrer"&gt;2.4.4 Link Purpose&lt;/a&gt;) and a real heading outline with landmarks (1.3.1) fix both, and the &lt;a href="https://getaccessguard.com/checks/link-text" rel="noopener noreferrer"&gt;link text checker&lt;/a&gt; finds the vague ones.&lt;/p&gt;

&lt;h2&gt;What helps agents that read screenshots&lt;/h2&gt;

&lt;p&gt;For a vision model, the page is a picture. What helps a person with low vision tends to help here too: text with enough contrast to read (&lt;a href="https://getaccessguard.com/posts/color-contrast-requirements-wcag-ada" rel="noopener noreferrer"&gt;contrast requirements&lt;/a&gt;), and click targets large enough and far enough apart to hit by coordinates (&lt;a href="https://getaccessguard.com/posts/wcag-2-5-8-target-size-minimum-24px" rel="noopener noreferrer"&gt;WCAG 2.5.8 target size&lt;/a&gt;). Text set as real HTML text, rather than baked into an image, is readable either way.&lt;/p&gt;

&lt;p&gt;Some things that block agents are outside WCAG altogether: bot checks, sign-in walls, rate limits, cookie banners that cover the page. Whether you want agents to get past those is a business decision, not an accessibility one, and an audit will not answer it.&lt;/p&gt;

&lt;h2&gt;How to check if your website is AI agent ready&lt;/h2&gt;

&lt;p&gt;You can see your site the way a tree-reading agent sees it in a few minutes, with tools you already have.&lt;/p&gt;

&lt;h3&gt;Look at the accessibility tree in Chrome&lt;/h3&gt;

&lt;p&gt;Open DevTools, go to the Elements panel, open the Accessibility tab and turn on &lt;a href="https://developer.chrome.com/docs/devtools/accessibility/reference#tab" rel="noopener noreferrer"&gt;Show accessibility tree&lt;/a&gt;. The DOM is replaced by the full page tree. Walk through your main task, such as add to cart or book a call, and look at every control on the way. Each one should have a role you would expect and a name you could act on. "button" with no name, "generic" where a button should be, and "textbox" with no name are the three things to look for.&lt;/p&gt;

&lt;h3&gt;Dump the tree with Playwright&lt;/h3&gt;

&lt;p&gt;If you write tests, &lt;a href="https://playwright.dev/docs/aria-snapshots" rel="noopener noreferrer"&gt;Playwright's ARIA snapshots&lt;/a&gt; give you "a YAML representation of the accessibility tree" of a page or part of it, one element per line with its role and name:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Prints the checkout form as an agent reading the tree sees it
const tree = await page.locator('form#checkout').ariaSnapshot();
console.log(tree);
// - textbox "Email"
// - textbox "ZIP or postal code"
// - button "Place order"&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Store the snapshot and compare it in CI, and a later release that drops a label or a name fails the build instead of reaching users.&lt;/p&gt;

&lt;h3&gt;Do the keyboard pass&lt;/h3&gt;

&lt;p&gt;Tab through the same task without a mouse. Anything you cannot reach or operate with the keyboard usually has a role problem underneath, and that is what trips a tree-reading agent too. Our &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;keyboard testing guide&lt;/a&gt; has the full procedure, and if the task opens a dialog, the &lt;a href="https://getaccessguard.com/posts/accessible-modals-how-to-build-dialogs-that-dont-trap-confuse-or-exclude-users" rel="noopener noreferrer"&gt;accessible modal dialog guide&lt;/a&gt; covers what it has to do.&lt;/p&gt;

&lt;h3&gt;Scan for the patterns&lt;/h3&gt;

&lt;p&gt;Our &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; runs automated WCAG 2.2 AA checks in a real browser and finds the unnamed buttons, unlabelled fields, clickable divs and vague links across a page in a couple of minutes. It does not give an agent readiness score, and it cannot tell you whether a particular agent can finish a particular task. Automated checks find many real problems but cannot confirm that a page conforms to WCAG, and nobody can promise that fixing them changes how any AI product treats your site, since they do not publish that.&lt;/p&gt;

&lt;h2&gt;What to tell a client or a manager&lt;/h2&gt;

&lt;p&gt;Keep it to three sentences. AI agents that browse for people read the same labels and structure screen readers use, and at least one of the companies building them, OpenAI, tells site owners to follow WAI-ARIA best practices for that reason. The problems that stop agents are WCAG failures we can find and fix, so the work pays off for disabled visitors whatever happens with agents. We will not promise more traffic from AI, because nobody can.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Every button and link has a name in the accessibility tree, including icon buttons and image links.&lt;/li&gt;
&lt;li&gt;Every interactive element is a real &lt;code&gt;button&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt; or &lt;code&gt;select&lt;/code&gt;, or has the right role and state.&lt;/li&gt;
&lt;li&gt;Every form field has a &lt;code&gt;label&lt;/code&gt;, and personal data fields have an &lt;code&gt;autocomplete&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;Accessible names contain the visible text.&lt;/li&gt;
&lt;li&gt;Links say where they go, and the page has headings and a &lt;code&gt;main&lt;/code&gt; landmark.&lt;/li&gt;
&lt;li&gt;Text has enough contrast and targets are at least 24 by 24 CSS pixels or spaced to match.&lt;/li&gt;
&lt;li&gt;You have walked your main task in the accessibility tree and with the keyboard.&lt;/li&gt;
&lt;li&gt;You have run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; on the pages that matter and fixed what it found.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Color Contrast Requirements: WCAG 2.0, 2.1, 2.2 and the ADA</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:14:02 +0000</pubDate>
      <link>https://dev.to/chille87/color-contrast-requirements-wcag-20-21-22-and-the-ada-3gci</link>
      <guid>https://dev.to/chille87/color-contrast-requirements-wcag-20-21-22-and-the-ada-3gci</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/color-contrast-requirements-wcag-ada" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;Someone has asked you what contrast ratio the law requires. Maybe a client who got a letter about the ADA, a designer defending a pale gray, or a manager who read that "WCAG 2.2 changed everything". The short answer is the same in almost every case: &lt;strong&gt;4.5:1 for normal text and 3:1 for large text&lt;/strong&gt;, plus 3:1 for the parts of buttons, inputs and icons people need to see. That has been the Level AA rule since WCAG 2.0, and no later version or US law has changed the numbers.&lt;/p&gt;

&lt;p&gt;If you only need to check a pair of colors, the &lt;a href="https://getaccessguard.com/checks/color-contrast" rel="noopener noreferrer"&gt;free color contrast checker&lt;/a&gt; gives you the ratio and the AA verdict in a few seconds. The rest of this guide explains where each number comes from, which version and which law points at which level, and what to say when someone asks whether a ratio makes a site "ADA compliant".&lt;/p&gt;

&lt;h2&gt;The ratios at a glance&lt;/h2&gt;

&lt;p&gt;Every number below comes from the WCAG 2.2 Recommendation and its Understanding documents at the W3C, linked in each row.&lt;/p&gt;

&lt;p&gt;WhatLevel AALevel AAACriterion&lt;/p&gt;

&lt;p&gt;Normal text and images of text4.5:17:1&lt;a href="https://www.w3.org/TR/WCAG22/#contrast-minimum" rel="noopener noreferrer"&gt;1.4.3&lt;/a&gt; (AA), &lt;a href="https://www.w3.org/TR/WCAG22/#contrast-enhanced" rel="noopener noreferrer"&gt;1.4.6&lt;/a&gt; (AAA)&lt;br&gt;
Large text (18pt, or 14pt bold)3:14.5:11.4.3 (AA), 1.4.6 (AAA)&lt;br&gt;
Parts of user interface components and graphics needed to understand them3:1No higher level&lt;a href="https://www.w3.org/TR/WCAG22/#non-text-contrast" rel="noopener noreferrer"&gt;1.4.11&lt;/a&gt; (AA)&lt;br&gt;
Focus indicator, focused against unfocused stateNot required3:1&lt;a href="https://www.w3.org/TR/WCAG22/#focus-appearance" rel="noopener noreferrer"&gt;2.4.13&lt;/a&gt; (AAA)&lt;/p&gt;

&lt;p&gt;Three details catch people out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No rounding.&lt;/strong&gt; The W3C says computed values "should not be rounded", and gives the example that 4.499:1 does not meet 4.5:1 (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;Understanding 1.4.3&lt;/a&gt;). A checker that shows 4.5 for a pair that is really 4.47 is hiding a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large text is measured in points.&lt;/strong&gt; The same document puts 1pt at 1.333 CSS pixels, so 18pt is about 24px and 14pt is about 18.5px. Bold at 18.5px counts as large; regular weight at 18.5px does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some text has no requirement.&lt;/strong&gt; Text in an inactive control, pure decoration, text nobody can see, text inside a photo with other significant content, and logos and brand names are all exempt under 1.4.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;WCAG 2.0, 2.1 and 2.2: what changed for contrast&lt;/h2&gt;

&lt;p&gt;Most searches about contrast name a version, as if each one set a new number. They did not. Here is what each version did.&lt;/p&gt;

&lt;h3&gt;WCAG 2.0 (2008)&lt;/h3&gt;

&lt;p&gt;WCAG 2.0 introduced both text criteria: &lt;a href="https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast" rel="noopener noreferrer"&gt;1.4.3 Contrast (Minimum)&lt;/a&gt; at AA with 4.5:1 and 3:1, and 1.4.6 Contrast (Enhanced) at AAA with 7:1 and 4.5:1. It said nothing about the contrast of borders, icons or focus outlines, so a pale gray input border passed 2.0 as long as its label text was dark enough.&lt;/p&gt;

&lt;h3&gt;WCAG 2.1 (2018)&lt;/h3&gt;

&lt;p&gt;WCAG 2.1 kept 1.4.3 and 1.4.6 as they were and added &lt;a href="https://www.w3.org/TR/WCAG21/#non-text-contrast" rel="noopener noreferrer"&gt;1.4.11 Non-text Contrast&lt;/a&gt; at Level AA, one of 17 new criteria (&lt;a href="https://www.w3.org/WAI/standards-guidelines/wcag/new-in-21/" rel="noopener noreferrer"&gt;W3C, What's new in WCAG 2.1&lt;/a&gt;). It asks for 3:1 against adjacent colors for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the visual information needed to identify a control and its state, such as an input's border, a checkbox's box or the fill of a toggle, unless the control is inactive or its look comes from the browser unchanged;&lt;/li&gt;
&lt;li&gt;the parts of a graphic needed to understand it, such as the lines in a chart or an icon that carries meaning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the change that matters most in practice. A site that met 2.0 can fail 2.1 without a single text color changing, because of light form borders and pale icons.&lt;/p&gt;

&lt;h3&gt;WCAG 2.2 (2023)&lt;/h3&gt;

&lt;p&gt;WCAG 2.2 changed none of the ratios. The W3C says the 2.0 and 2.1 criteria "are essentially the same in 2.2, with one exception: 4.1.1 Parsing is obsolete and removed" (&lt;a href="https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/" rel="noopener noreferrer"&gt;What's new in WCAG 2.2&lt;/a&gt;). Of the nine new criteria, one involves a ratio: &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html" rel="noopener noreferrer"&gt;2.4.13 Focus Appearance&lt;/a&gt;, which asks for a focus indicator at least as large as a 2 CSS pixel perimeter of the component, with 3:1 between the same pixels focused and unfocused. It is Level AAA, so it is not part of an AA target.&lt;/p&gt;

&lt;p&gt;So if you test against WCAG 2.2 AA, you are testing the same contrast rules as 2.1 AA. If you test against 2.0 AA, you are missing 1.4.11.&lt;/p&gt;

&lt;h2&gt;Why 4.5:1 and not some other number&lt;/h2&gt;

&lt;p&gt;Clients sometimes treat the number as arbitrary. The W3C explains it in &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;Understanding 1.4.3&lt;/a&gt;: 3:1 is the minimum that the ISO 9241-3 and ANSI/HFES 100-1988 standards recommend for standard text and vision, and the ratio is raised to 4.5:1 to make up for the contrast lost by someone with moderately low visual acuity, about 20/40. That is a useful line for a client meeting: the rule is sized for a common level of vision loss, not for rare cases.&lt;/p&gt;

&lt;h2&gt;What the ADA and other laws actually require&lt;/h2&gt;

&lt;p&gt;No US law sets a contrast ratio of its own. Each one points at WCAG, and the version and level it points at decide which rows of the table apply.&lt;/p&gt;

&lt;p&gt;RuleWho it coversStandard it points to&lt;/p&gt;

&lt;p&gt;ADA Title II web ruleUS state and local governments, including public schools and collegesWCAG 2.1 AA (&lt;a href="https://www.federalregister.gov/documents/2024/04/24/2024-07758/nondiscrimination-on-the-basis-of-disability-accessibility-of-web-information-and-services-of-state" rel="noopener noreferrer"&gt;89 FR 31320&lt;/a&gt;)&lt;br&gt;
ADA Title IIIBusinesses open to the publicNo technical standard in the regulations; the Justice Department calls WCAG "helpful guidance" (&lt;a href="https://www.ada.gov/resources/web-guidance/" rel="noopener noreferrer"&gt;ADA.gov, 2022&lt;/a&gt;)&lt;br&gt;
Section 508US federal agencies and what they buyWCAG 2.0 AA (&lt;a href="https://www.access-board.gov/ict/" rel="noopener noreferrer"&gt;Access Board, E205.4&lt;/a&gt;)&lt;br&gt;
European Accessibility ActMany products and services sold in the EUEN 301 549; version 4.1.1, published in September 2026, points to WCAG 2.2 AA (&lt;a href="https://accessible-eu-centre.ec.europa.eu/content-corner/news/european-accessibility-standard-en-301-549-has-been-updated-2026-09-07_en" rel="noopener noreferrer"&gt;AccessibleEU&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A few practical readings of that table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title II sites need 1.4.11.&lt;/strong&gt; Because the rule adopts 2.1 AA, a city or college site has to meet the 3:1 non-text rule as well as the text ratios. The compliance dates moved to April 2027 and April 2028 in an interim rule this year; our guide to the &lt;a href="https://getaccessguard.com/posts/ada-title-ii-deadline-extension-2027" rel="noopener noreferrer"&gt;Title II deadline extension&lt;/a&gt; has the details, and the &lt;a href="https://getaccessguard.com/ada-title-ii-web-rule" rel="noopener noreferrer"&gt;ADA Title II web rule&lt;/a&gt; page has the summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title III has no number in law.&lt;/strong&gt; When someone says "the ADA requires 4.5:1", what is true is that WCAG AA requires it and WCAG AA is the benchmark most businesses use. The Justice Department's own guidance names "poor color contrast" as an example of a barrier, which is a good reason to treat it as a priority. See &lt;a href="https://getaccessguard.com/ada-website-compliance" rel="noopener noreferrer"&gt;ADA website compliance&lt;/a&gt; for how Title III works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section 508 still cites 2.0.&lt;/strong&gt; A federal client's formal requirement does not include 1.4.11, but testing against 2.1 or 2.2 AA covers everything 2.0 asks for and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody's law requires AAA.&lt;/strong&gt; None of these rules points at Level AAA, and the W3C itself does not recommend requiring AAA as a general policy for entire sites, because some content cannot meet it (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/conformance#levels" rel="noopener noreferrer"&gt;Understanding Conformance&lt;/a&gt;). 7:1 is a good choice for body text when the brand allows it, not an obligation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What to tell a client or a manager&lt;/h2&gt;

&lt;p&gt;Clients usually want one sentence. Something like this is accurate:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;"WCAG Level AA, which the ADA rules and guidance point to, asks for 4.5:1 on body text, 3:1 on large headings and 3:1 on borders and icons people need to see. Our current gray fails that, and darkening it fixes it without changing the look of the brand."&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;What not to say: that meeting the ratios makes a site compliant. Contrast covers two of the 55 Level A and AA success criteria in &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt; (our &lt;a href="https://getaccessguard.com/posts/wcag-2-2-checklist-level-a-and-aa" rel="noopener noreferrer"&gt;WCAG 2.2 checklist&lt;/a&gt; lists them all), and passing one says nothing about the others. The same goes for any scan, ours included: an automated check measures text on solid backgrounds reliably, but text over photos and gradients needs a person to judge.&lt;/p&gt;

&lt;h2&gt;How to check a site against these numbers&lt;/h2&gt;

&lt;p&gt;For a single pair of colors, a calculator is enough. For a site, the failures are usually in places nobody picked colors on purpose: placeholder text, disabled-looking buttons that are still active, light borders on inputs and icon-only links.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: #999 on white is 2.85:1, fails 4.5:1 for body text */
.form-hint {
  color: #999999;
}

/* Fixed: #767676 on white is 4.54:1, passes AA for normal text */
.form-hint {
  color: #767676;
}

/* Broken: #ddd input border on white is 1.36:1, fails 1.4.11 */
input {
  border: 1px solid #dddddd;
}

/* Fixed: #8a8a8a on white is 3.45:1, passes 3:1 for non-text contrast */
input {
  border: 1px solid #8a8a8a;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Run the ratios in your own checker before you ship them; brand colors rarely land on these exact grays. When a brand color fails, our &lt;a href="https://getaccessguard.com/posts/color-contrast-and-accessibility-a-designers-guide" rel="noopener noreferrer"&gt;guide to WCAG color contrast ratios&lt;/a&gt; walks through darkening it step by step, testing text over images, and handling a client who will not change the palette.&lt;/p&gt;

&lt;p&gt;To find every failing pair on a page at once, &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;run a free audit&lt;/a&gt;. AccessGuard runs automated WCAG 2.2 AA checks in a real browser, so it measures the colors the page actually renders, including ones set by a theme or a plugin, and lists each failing element with its ratio.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Decide the target first: WCAG 2.1 AA for a Title II client, 2.2 AA for anyone selling into the EU, and 2.2 AA as a sensible default for everyone else, since it covers the older versions' contrast rules.&lt;/li&gt;
&lt;li&gt;Check body text at 4.5:1 and large text (24px, or about 18.5px bold) at 3:1. Do not round.&lt;/li&gt;
&lt;li&gt;Check input borders, checkboxes, toggles, meaningful icons and chart lines at 3:1 against what is next to them.&lt;/li&gt;
&lt;li&gt;Check hover, focus and error states, not only the default state.&lt;/li&gt;
&lt;li&gt;Have a person look at text over images and gradients.&lt;/li&gt;
&lt;li&gt;Write the target version and level into the brief or the contract, so "compliant" never becomes the promise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the &lt;a href="https://getaccessguard.com/checks/color-contrast" rel="noopener noreferrer"&gt;color contrast checker&lt;/a&gt; for the brand palette, then &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;scan the pages that matter&lt;/a&gt; to see where those colors end up.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>wcag</category>
    </item>
    <item>
      <title>WCAG 2.5.8 Target Size: The 24px Minimum and Its Exceptions</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:13:26 +0000</pubDate>
      <link>https://dev.to/chille87/wcag-258-target-size-the-24px-minimum-and-its-exceptions-4cla</link>
      <guid>https://dev.to/chille87/wcag-258-target-size-the-24px-minimum-and-its-exceptions-4cla</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/wcag-2-5-8-target-size-minimum-24px" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;A WCAG 2.2 scan flags a row of small icons, a pagination bar and a close button, and the question that follows is always the same: do we really have to make all of these 24 pixels? Often you don't. The number in 2.5.8 is short, but the exceptions do most of the work, and the spacing exception alone clears a lot of controls without touching their size.&lt;/p&gt;

&lt;p&gt;This guide gives the exact rule, the spacing exception with real measurements, the patterns that fail most often, and fixes that leave the design alone. To see which controls on a page are too small right now, run the page through our &lt;a href="https://getaccessguard.com/checks/target-size" rel="noopener noreferrer"&gt;target size checker&lt;/a&gt;. It measures every button and link in a browser against the 24 by 24 minimum.&lt;/p&gt;

&lt;h2&gt;What the criterion requires&lt;/h2&gt;

&lt;p&gt;Success Criterion 2.5.8 Target Size (Minimum) is Level AA and was added in WCAG 2.2, which W3C published as a Recommendation in October 2023 (&lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;W3C, WCAG 2.2&lt;/a&gt;). Its text, from &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;W3C's Understanding 2.5.8&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;"The size of the target for pointer inputs is at least 24 by 24 CSS pixels, except when:"&lt;/p&gt;&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spacing:&lt;/strong&gt; the target is smaller, but a 24 CSS pixel circle centered on it does not touch another target or another small target's circle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equivalent:&lt;/strong&gt; "The function can be achieved through a different control on the same page that meets this criterion."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline:&lt;/strong&gt; "The target is in a sentence or its size is otherwise constrained by the line-height of non-target text."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User agent control:&lt;/strong&gt; "The size of the target is determined by the user agent and is not modified by the author."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Essential:&lt;/strong&gt; "A particular presentation of the target is essential or is legally required for the information being conveyed."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three details matter before you measure anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both dimensions count.&lt;/strong&gt; W3C calls a target "undersized" when it is smaller than 24 by 24. A footer link 120 pixels wide and 18 pixels tall is undersized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The target is the clickable area, not the icon.&lt;/strong&gt; A 16 pixel icon inside a 24 pixel button passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS pixels, not device pixels.&lt;/strong&gt; This is the size in your stylesheet and in the browser's dev tools, whatever the screen density.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;24 is not 44&lt;/h3&gt;

&lt;p&gt;The 44 by 44 figure you see in design systems belongs to 2.5.5 Target Size (Enhanced), which is Level AAA: "The size of the target for pointer inputs is at least 44 by 44 CSS pixels" (&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html" rel="noopener noreferrer"&gt;W3C, Understanding 2.5.5&lt;/a&gt;). An AA audit tests 24. W3C still suggests aiming for the larger size "for important links/controls", and that is good advice for a primary button. It is not a failure of AA.&lt;/p&gt;

&lt;h2&gt;The spacing exception, measured&lt;/h2&gt;

&lt;p&gt;This is the exception that saves most redesigns, and it is easier to apply than it reads. W3C's test (&lt;a href="https://www.w3.org/WAI/WCAG22/Techniques/css/C42" rel="noopener noreferrer"&gt;technique C42&lt;/a&gt;) for each undersized target:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw a 24 CSS pixel diameter circle centered on the target's bounding box.&lt;/li&gt;
&lt;li&gt;Check that the circle does not intersect any other target.&lt;/li&gt;
&lt;li&gt;Check that the circle does not intersect any other undersized target's circle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both checks pass, and the small target passes. Turned into numbers for targets side by side, two rules of thumb fall out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two small targets next to each other&lt;/strong&gt; pass when their centers are at least 24 pixels apart. For equal targets, that is &lt;em&gt;size plus gap is at least 24&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small target next to a full size one&lt;/strong&gt; passes when the gap is at least 12 minus half the small target's size, because its circle (radius 12) must not reach the bigger target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LayoutDistance between centersResult&lt;/p&gt;

&lt;p&gt;20 by 20 buttons, no gap20pxFails: circles overlap&lt;br&gt;
20 by 20 buttons, 4px gap24pxPasses (W3C's own example)&lt;br&gt;
16 by 16 icons, 4px gap20pxFails&lt;br&gt;
16 by 16 icons, 8px gap24pxPasses&lt;br&gt;
16 by 16 icon, 4px from a 40px buttonCircle reaches 12px, button starts 12px awayPasses&lt;br&gt;
Stacked links 18px tall, no gap18pxFails&lt;br&gt;
Stacked links 18px tall, 6px gap24pxPasses&lt;/p&gt;

&lt;p&gt;Two edge cases from the Understanding document. If targets overlap, the overlapping area is left out of the size. If a target is not a rectangle (rounded, clipped or an SVG shape), the circle goes on the center of its bounding box, which for an odd shape may sit outside the shape itself.&lt;/p&gt;

&lt;p&gt;One honest caveat: passing on spacing does not make a control easy to use. W3C says so directly: "It is still possible to have very small, and difficult to activate, targets and meet the requirements of this Success Criterion." Spacing is a pass, not a recommendation.&lt;/p&gt;

&lt;h2&gt;The inline, user agent and essential exceptions&lt;/h2&gt;

&lt;h3&gt;Inline&lt;/h3&gt;

&lt;p&gt;Links inside a sentence are exempt. W3C's reason is that text reflows with the viewport, so an author cannot know where links will land next to each other. The fix for crowded text links is readable line height, not bigger links. Note what this does &lt;em&gt;not&lt;/em&gt; cover: a list of links stacked in a footer or sidebar is not a sentence, so treat each one as a target and measure it.&lt;/p&gt;

&lt;h3&gt;User agent control&lt;/h3&gt;

&lt;p&gt;If the browser draws the control and you have not resized it, it is exempt. W3C's examples are the day grid in &lt;code&gt;&amp;lt;input type="date"&amp;gt;&lt;/code&gt; and the browser's scrollbars. The exception ends the moment your CSS changes the size: a native checkbox shrunk to 12 pixels, or a restyled thin scrollbar, is yours to measure.&lt;/p&gt;

&lt;h3&gt;Essential&lt;/h3&gt;

&lt;p&gt;This applies when the position or size carries the information. W3C's example is pins on a map: they must sit where the places are, even when that puts them close together. Forms that law requires to copy a paper layout are the other example. "Our designer prefers small icons" is not essential.&lt;/p&gt;

&lt;h3&gt;Equivalent&lt;/h3&gt;

&lt;p&gt;A small control passes if the same function is available through another control on the same page that meets the criterion. A tiny "edit" icon in a table row can pass if the row also opens a full size edit menu. Write down which control is the equivalent in the audit, because a reviewer will ask.&lt;/p&gt;

&lt;h2&gt;Five patterns that usually fail&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Icon-only social rows.&lt;/strong&gt; Sixteen pixel icons with a few pixels between them. Under 24 pixels between centers, so they fail on spacing too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table row actions.&lt;/strong&gt; Edit, duplicate and delete icons packed at the end of each row. They fail unless a full size equivalent exists, and a slip here deletes the wrong record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close buttons.&lt;/strong&gt; A 14 pixel "x" in a dialog corner. Often it passes on spacing because nothing else is near it, so check the circle before you file it. It is still worth enlarging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination.&lt;/strong&gt; Single digit page numbers are usually narrower than 24 pixels, with little space between them. This is the case technique C42 was written for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Footer link lists.&lt;/strong&gt; Wide but short, stacked with no space between them. The height is the problem, and the inline exception does not apply.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;How to fix without redesigning&lt;/h2&gt;

&lt;p&gt;Almost every fix makes the clickable area bigger and leaves the visible icon the same size.&lt;/p&gt;

&lt;h3&gt;Give the link the size, not the icon&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: the link is only as big as its 16px icon */
.social a {
  display: inline-block;
}

/* Fixed: the icon stays 16px, the target is 24px */
.social a {
  display: inline-grid;
  place-items: center;
  min-width: 24px;
  min-height: 24px;
}
.social a svg {
  width: 16px;
  height: 16px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Or use the spacing exception on purpose&lt;/h3&gt;

&lt;p&gt;When a toolbar really has to stay compact, set the gap from the table above instead of guessing. For 16 pixel icons that is an 8 pixel gap.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: 16px buttons, 4px apart, 20px between centers */
.row-actions {
  display: flex;
  gap: 4px;
}

/* Fixed: 8px apart, 24px between centers */
.row-actions {
  display: flex;
  gap: 8px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Pagination and footer lists: minimums on the container&lt;/h3&gt;

&lt;p&gt;This is W3C's own technique C42, minimum sizes on each item:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Fixed: every page number and footer link is at least 24 by 24 */
.pagination li,
.footer-links li {
  min-height: 24px;
  min-width: 24px;
}
.pagination a,
.footer-links a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Put the size on the &lt;code&gt;a&lt;/code&gt; as well as the &lt;code&gt;li&lt;/code&gt;. A 24 pixel list item with an 18 pixel link inside it still has an 18 pixel target, because only the link responds to the click.&lt;/p&gt;

&lt;h3&gt;Checkboxes and radios you restyled&lt;/h3&gt;

&lt;p&gt;A native checkbox at the browser's default size is a user agent control and exempt. A custom one is not: once your CSS draws the box, the box is yours to size.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: a custom checkbox drawn at 14px */
.check input {
  appearance: none;
  width: 14px;
  height: 14px;
}

/* Fixed: the same style at the minimum size */
.check input {
  appearance: none;
  width: 24px;
  height: 24px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wrap the input in its &lt;code&gt;label&lt;/code&gt; while you are there, so the text is clickable too and the label association a form audit checks is in place. Our &lt;a href="https://getaccessguard.com/posts/accessible-forms-how-to-build-inputs-every-user-can-complete" rel="noopener noreferrer"&gt;guide to accessible forms&lt;/a&gt; covers that part.&lt;/p&gt;

&lt;h2&gt;What to tell a client or a manager&lt;/h2&gt;

&lt;p&gt;Keep it to three sentences. WCAG 2.2 added a Level AA rule that buttons and links must be at least 24 pixels, or spaced so a slip does not hit the neighbor. It helps anyone tapping on a phone, people with tremors and people using a device on a moving bus, all groups W3C names. The fix is usually CSS on the click area and nobody will see a design change.&lt;/p&gt;

&lt;p&gt;Be careful with the words you use: fixing target sizes clears one criterion. It does not make a site compliant, and neither does a clean automated scan. The &lt;a href="https://getaccessguard.com/posts/wcag-2-2-checklist-level-a-and-aa" rel="noopener noreferrer"&gt;WCAG 2.2 checklist&lt;/a&gt; lists every A and AA criterion, and our &lt;a href="https://getaccessguard.com/posts/full-wcag-22-coverage-what-we-added-and-why-most-scanners-miss-it" rel="noopener noreferrer"&gt;guide to the nine new WCAG 2.2 criteria&lt;/a&gt; puts this one next to the others 2.2 added.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Run the page through the &lt;a href="https://getaccessguard.com/checks/target-size" rel="noopener noreferrer"&gt;target size checker&lt;/a&gt;, or a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility audit&lt;/a&gt; for the rest of WCAG 2.2 AA at the same time.&lt;/li&gt;
&lt;li&gt;For each flagged control, check the exceptions in order: is it in a sentence, drawn by the browser, essential, or duplicated by a full size control?&lt;/li&gt;
&lt;li&gt;For the rest, measure the distance between centers. 24 pixels or more passes on spacing.&lt;/li&gt;
&lt;li&gt;Fix with &lt;code&gt;min-width&lt;/code&gt;, &lt;code&gt;min-height&lt;/code&gt; and padding on the clickable element, not on the icon.&lt;/li&gt;
&lt;li&gt;Recheck at a phone width, where layouts often draw controls smaller.&lt;/li&gt;
&lt;li&gt;Treat 44 by 44 as the goal for primary actions, and 24 as the line an AA audit draws.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>Lighthouse Accessibility Score: Why 100 Is Not an Audit</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:15:45 +0000</pubDate>
      <link>https://dev.to/chille87/lighthouse-accessibility-score-why-100-is-not-an-audit-495a</link>
      <guid>https://dev.to/chille87/lighthouse-accessibility-score-why-100-is-not-an-audit-495a</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/lighthouse-accessibility-score-why-100-is-not-an-audit" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;p&gt;Sooner or later a client, a manager or a colleague opens Chrome DevTools, runs Lighthouse, sees a green 100 in the Accessibility circle, and asks why anyone needs an audit. It is a fair question, and it deserves a better answer than "trust me". This guide explains what the Lighthouse accessibility score measures, what it cannot see, and gives you words to use in that conversation.&lt;/p&gt;

&lt;p&gt;Short version: a 100 means the page passed every automated test Lighthouse ran on it. That is worth having. It does not mean people with disabilities can use the page, and the Lighthouse report says so itself, in the line directly under the score: "Automatic detection can only detect a subset of issues and does not guarantee the accessibility of your web app, so manual testing is also encouraged."&lt;/p&gt;

&lt;h2&gt;What the Lighthouse accessibility score actually measures&lt;/h2&gt;

&lt;p&gt;Lighthouse runs a set of automated audits against one URL. In Google's words, you "give Lighthouse a URL to audit, it runs a series of audits against the page, and then it generates a report" (&lt;a href="https://developer.chrome.com/docs/lighthouse/overview" rel="noopener noreferrer"&gt;Lighthouse overview&lt;/a&gt;). Three details in the &lt;a href="https://developer.chrome.com/docs/lighthouse/accessibility/scoring" rel="noopener noreferrer"&gt;Lighthouse accessibility scoring documentation&lt;/a&gt; matter for how you read the number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is a weighted average.&lt;/strong&gt; "The Lighthouse Accessibility score is a weighted average of all accessibility audits. Weighting is based on axe user impact assessments." The audits come from axe-core, Deque's open source testing engine, and each carries a weight of 10, 7 or 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each audit is pass or fail.&lt;/strong&gt; A page with one unnamed button out of twenty scores the same zero on that audit as a page where all twenty are unnamed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual checks do not count.&lt;/strong&gt; "Manual audits and low-impact / best-practices audits aren't included in the table because they don't affect your score."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more things follow from how the tool runs. It tests the page as it looks when it finishes loading, in the viewport you chose, so a menu that is closed, a modal that has not opened and an error message that only appears after submitting a form are never tested. And it tests one page, not the checkout, the account area or the other templates on the site.&lt;/p&gt;

&lt;h2&gt;The checks Google lists as manual&lt;/h2&gt;

&lt;p&gt;This is the most useful fact for the client conversation, because it comes from Google rather than from someone selling an audit. Below the score, every Lighthouse report has a section called "Additional items to manually check" with ten items, each described in the &lt;a href="https://developer.chrome.com/docs/lighthouse/accessibility" rel="noopener noreferrer"&gt;Lighthouse accessibility audits docs&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom controls have ARIA roles&lt;/li&gt;
&lt;li&gt;Custom controls have associated labels&lt;/li&gt;
&lt;li&gt;User focus is not accidentally trapped in a region&lt;/li&gt;
&lt;li&gt;Interactive controls are keyboard focusable&lt;/li&gt;
&lt;li&gt;Interactive elements indicate their purpose and state&lt;/li&gt;
&lt;li&gt;The page has a logical tab order&lt;/li&gt;
&lt;li&gt;The user's focus is directed to new content added to the page&lt;/li&gt;
&lt;li&gt;Offscreen content is hidden from assistive technology&lt;/li&gt;
&lt;li&gt;HTML5 landmark elements are used to improve navigation&lt;/li&gt;
&lt;li&gt;Visual order on the page follows DOM order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these affect the score. A page can fail every one and still show 100. Most of them are about keyboard use, which is exactly where people who cannot use a mouse get stuck.&lt;/p&gt;

&lt;h2&gt;Four things a 100 cannot tell you&lt;/h2&gt;

&lt;h3&gt;1. Whether the alt text is right&lt;/h3&gt;

&lt;p&gt;The scored audit is "Image elements have [alt] attributes". It checks that the attribute exists. It cannot tell whether the text describes the image. Both of these pass:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: passes the audit, tells a screen reader user nothing --&amp;gt;
&amp;lt;img src="team.jpg" alt="IMG_2041.jpg"&amp;gt;

&amp;lt;!-- Fixed: describes what the image shows in this context --&amp;gt;
&amp;lt;img src="team.jpg" alt="Our four support staff at the Leeds office"&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Whether a text alternative "serves the equivalent purpose" is the actual requirement in &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html" rel="noopener noreferrer"&gt;WCAG 1.1.1 Non-text Content&lt;/a&gt;, and that takes a person. An &lt;a href="https://getaccessguard.com/checks/alt-text" rel="noopener noreferrer"&gt;alt text checker&lt;/a&gt; finds the missing ones fast; our guide on &lt;a href="https://getaccessguard.com/posts/alt-text-done-right-writing-image-descriptions-that-work" rel="noopener noreferrer"&gt;how to write alt text&lt;/a&gt; covers the rest.&lt;/p&gt;

&lt;h3&gt;2. Whether keyboard focus gets trapped&lt;/h3&gt;

&lt;p&gt;"User focus is not accidentally trapped in a region" is on Google's manual list. Its own documentation tells you how to test it: "navigate to and from all page elements using only the keyboard", with Tab going forward and Shift + Tab going back (&lt;a href="https://developer.chrome.com/docs/lighthouse/accessibility/focus-traps" rel="noopener noreferrer"&gt;Lighthouse: trapped user focus&lt;/a&gt;). A date picker or chat widget that swallows focus fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html" rel="noopener noreferrer"&gt;2.1.2 No Keyboard Trap&lt;/a&gt; while the score stays at 100. Our guide on &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;how to test keyboard accessibility by hand&lt;/a&gt; walks through it, and the &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; flags the markup causes it can see.&lt;/p&gt;

&lt;h3&gt;3. Whether the reading order makes sense&lt;/h3&gt;

&lt;p&gt;CSS grid, flexbox &lt;code&gt;order&lt;/code&gt; and absolute positioning can put things on screen in a different order from the HTML. A screen reader and the Tab key follow the HTML. If the "Add to cart" button comes before the product name in the source, it passes every automated audit and still confuses people. That is &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/meaningful-sequence.html" rel="noopener noreferrer"&gt;1.3.2 Meaningful Sequence&lt;/a&gt; and &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html" rel="noopener noreferrer"&gt;2.4.3 Focus Order&lt;/a&gt;, and "Visual order on the page follows DOM order" sits on the manual list.&lt;/p&gt;

&lt;h3&gt;4. Whether a control's name matches what people see&lt;/h3&gt;

&lt;p&gt;The scored audit "Buttons have an accessible name" passes if there is any name at all. It does not compare that name with the visible label:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: has a name, but not the one on screen --&amp;gt;
&amp;lt;button aria-label="Submit form"&amp;gt;Get my quote&amp;lt;/button&amp;gt;

&amp;lt;!-- Fixed: the name contains the visible words --&amp;gt;
&amp;lt;button&amp;gt;Get my quote&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A voice control user who says "click Get my quote" gets nothing, because the button is called "Submit form". That fails &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html" rel="noopener noreferrer"&gt;2.5.3 Label in Name&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;Checks that need a person, by WCAG criterion&lt;/h2&gt;

&lt;p&gt;This is not a complete list, but it covers what most reviews catch that automated scores miss.&lt;/p&gt;

&lt;p&gt;WCAG criterionWhat a person checks&lt;/p&gt;

&lt;p&gt;1.1.1 Non-text ContentAlt text describes the image; decorative images are empty&lt;br&gt;
1.3.2 Meaningful SequenceReading order matches the visual order&lt;br&gt;
2.1.1 Keyboard, 2.1.2 No Keyboard TrapEvery control works with Tab, Enter, Space and Escape, and focus can always leave&lt;br&gt;
2.4.3 Focus OrderTab moves through the page in a sensible order, including into and out of modals&lt;br&gt;
2.4.6 Headings and LabelsHeadings and labels describe their content, not just exist&lt;br&gt;
2.5.3 Label in NameThe accessible name contains the visible label&lt;br&gt;
3.3.1 Error IdentificationErrors after submitting are described in text and announced&lt;br&gt;
4.1.3 Status MessagesMessages like "Added to cart" reach screen readers without moving focus&lt;/p&gt;

&lt;p&gt;Every criterion links from the &lt;a href="https://www.w3.org/TR/WCAG22/" rel="noopener noreferrer"&gt;WCAG 2.2 specification&lt;/a&gt; to its Understanding document, which says what passing looks like.&lt;/p&gt;

&lt;h2&gt;What to say when someone points at the score&lt;/h2&gt;

&lt;p&gt;Keep it calm and give them Google's own words. The best ones are already on their screen, under the score: "Automatic detection can only detect a subset of issues and does not guarantee the accessibility of your web app, so manual testing is also encouraged." Something like:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;"The 100 is real and it is good news: the page passes every automated test Lighthouse runs. The report itself says automatic detection only finds a subset of issues. It also lists ten things to check by hand, like keyboard traps and tab order, and Google's documentation says those don't affect the score. So a 100 tells us the basics are in place on this one page as it loaded. The audit is the part a tool can't do: someone using the site with a keyboard and a screen reader, across the templates that matter, like checkout and the contact form."&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;If they want evidence that tools miss things, the UK Government Digital Service built a page with 143 deliberate barriers and ran 10 automated tools against it in 2017. &lt;a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/" rel="noopener noreferrer"&gt;42 of the barriers were missed by every tool&lt;/a&gt;. Tools have improved since, so treat it as a demonstration of the gap rather than a current measurement. Avoid quoting a single percentage of issues that automation catches unless you can name the study behind it.&lt;/p&gt;

&lt;p&gt;Two things not to say: that an audit will make the site "compliant", or that the score is worthless. Neither is true, and the second one undermines the tools you will use yourself.&lt;/p&gt;

&lt;h2&gt;Where an automated scan still earns its keep&lt;/h2&gt;

&lt;p&gt;Automated checks are fast, repeatable and good at the problems they can decide: missing alt attributes, unlabelled fields, low contrast, invalid ARIA, missing page language. Run them first so the manual review is spent on judgement, not on counting empty alt attributes. Rerun them after every release so fixed problems stay fixed.&lt;/p&gt;

&lt;p&gt;The limits of Lighthouse are one page at a time, in one state. A &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free AccessGuard scan&lt;/a&gt; runs axe-core plus its own checks in a real browser and reports failures against WCAG 2.2 AA, including some that axe alone does not catch (see &lt;a href="https://getaccessguard.com/posts/6-accessibility-checks-most-scanners-miss-and-how-accessguard-catches-them" rel="noopener noreferrer"&gt;the six checks most scanners miss&lt;/a&gt;). It is still an automated scan: it finds many problems, and it cannot confirm that a site conforms to WCAG. No automated tool can.&lt;/p&gt;

&lt;h2&gt;Your next steps&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run Lighthouse or a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free scan&lt;/a&gt; and fix every automated failure first.&lt;/li&gt;
&lt;li&gt;Open the "Additional items to manually check" section of the Lighthouse report and work through it.&lt;/li&gt;
&lt;li&gt;Tab through each key template: home, a content page, search, forms, checkout.&lt;/li&gt;
&lt;li&gt;Read the alt text on important images out loud: does it say what the image shows?&lt;/li&gt;
&lt;li&gt;Compare visible button labels with their accessible names in DevTools.&lt;/li&gt;
&lt;li&gt;Write down what you checked by hand, so the report shows the work behind the result, not just a score.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Keyboard Navigation Accessibility: A Complete WCAG Guide</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:16:33 +0000</pubDate>
      <link>https://dev.to/chille87/keyboard-navigation-accessibility-a-complete-wcag-guide-4a6c</link>
      <guid>https://dev.to/chille87/keyboard-navigation-accessibility-a-complete-wcag-guide-4a6c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://getaccessguard.com/posts/keyboard-navigation-building-websites-everyone-can-use" rel="noopener noreferrer"&gt;AccessGuard&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;Not everyone uses a mouse&lt;/h2&gt;

&lt;p&gt;It's easy to assume that everyone navigates the web the way you do - clicking with a mouse or tapping on a touchscreen. But many people rely on a keyboard as their primary or only means of interaction.&lt;/p&gt;

&lt;p&gt;People with motor disabilities may use a keyboard, a switch device, or voice recognition software - all of which interact with web pages through the same keyboard interface. Screen reader users navigate almost entirely by keyboard. Power users often prefer keyboard shortcuts for speed. And anyone with a temporarily broken mouse or trackpad needs keyboard access as a fallback.&lt;/p&gt;

&lt;p&gt;Keyboard accessibility isn't an edge case. It's the foundation that makes all other assistive technologies work. If your site isn't keyboard accessible, it's likely inaccessible to screen readers, voice control, and switch devices as well.&lt;/p&gt;

&lt;h2&gt;How keyboard navigation works&lt;/h2&gt;

&lt;p&gt;Understanding the basics of keyboard navigation helps you test and build accessible interfaces.&lt;/p&gt;

&lt;h3&gt;The essential keys&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Tab&lt;/strong&gt; - moves focus to the next interactive element (links, buttons, form fields)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Shift + Tab&lt;/strong&gt; - moves focus to the previous interactive element&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Enter&lt;/strong&gt; - activates the focused element (follows links, submits buttons)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Space&lt;/strong&gt; - activates buttons, toggles checkboxes, opens select menus&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Arrow keys&lt;/strong&gt; - navigate within components (radio groups, &lt;a href="https://getaccessguard.com/posts/how-to-build-accessible-custom-dropdowns-comboboxes-that-actually-work" rel="noopener noreferrer"&gt;dropdown menus&lt;/a&gt;, tabs, sliders)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Escape&lt;/strong&gt; - closes &lt;a href="https://getaccessguard.com/posts/accessible-modals-how-to-build-dialogs-that-dont-trap-confuse-or-exclude-users" rel="noopener noreferrer"&gt;modal dialogs&lt;/a&gt;, menus, and popups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With just these keys, a user should be able to reach and operate every interactive element on your page. If they can't, you have a keyboard accessibility problem.&lt;/p&gt;

&lt;h3&gt;Focus order&lt;/h3&gt;

&lt;p&gt;When a user presses Tab, focus moves through interactive elements in &lt;strong&gt;DOM order&lt;/strong&gt; - the order elements appear in your HTML source code. This is why it's important that your visual layout matches your source order. If CSS visually rearranges elements (using flexbox order, grid placement, or absolute positioning), the tab order might not match what a sighted user expects.&lt;/p&gt;

&lt;p&gt;The fix is to change the source order, not to patch it with &lt;code&gt;tabindex&lt;/code&gt;. A positive &lt;code&gt;tabindex&lt;/code&gt; (1 or higher) pulls an element ahead of everything else on the page and almost always makes the order worse. WCAG 2.4.3 Focus Order (Level A) asks that focus moves "in an order that preserves meaning and operability" (&lt;a href="https://www.w3.org/TR/WCAG22/#focus-order" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: tabindex forces this link ahead of the whole header --&amp;gt;
&amp;lt;a href="/offer" tabindex="3"&amp;gt;Today's offer&amp;lt;/a&amp;gt;

&amp;lt;!-- Fixed: move it in the source, and leave tabindex out --&amp;gt;
&amp;lt;a href="/offer"&amp;gt;Today's offer&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Focus indicators - making focus visible&lt;/h2&gt;

&lt;p&gt;When a keyboard user tabs through a page, they need to see which element currently has focus. This visual indicator is called the &lt;strong&gt;focus ring&lt;/strong&gt; or &lt;strong&gt;focus indicator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Browsers provide a default focus indicator (usually a blue or dotted outline), but many websites remove it with CSS like &lt;code&gt;outline: none&lt;/code&gt; or &lt;code&gt;*:focus { outline: 0; }&lt;/code&gt; because designers find it aesthetically displeasing. This is one of the most harmful accessibility patterns on the web - it makes keyboard navigation essentially unusable.&lt;/p&gt;

&lt;h3&gt;Best practices for focus indicators&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Never remove focus indicators without providing a replacement.&lt;/strong&gt; If you remove the default outline, you must add a custom one that's equally visible.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Make focus indicators visible against all backgrounds.&lt;/strong&gt; A thin blue outline works on white but may be invisible on a blue background. Use a combination of outline offset, contrasting colors, or multiple indicators.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;:focus-visible&lt;/code&gt; for the best of both worlds.&lt;/strong&gt; The &lt;code&gt;:focus-visible&lt;/code&gt; pseudo-class only applies focus styles when focus was triggered by keyboard (not mouse click), reducing visual noise for mouse users while keeping indicators for keyboard users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good custom focus indicator might look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Broken: removes the indicator and replaces it with nothing */
*:focus { outline: none; }

/* Fixed: a visible ring for keyboard users, no ring on mouse clicks */
:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 2px;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://getaccessguard.com/posts/full-wcag-22-coverage-what-we-added-and-why-most-scanners-miss-it" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt; added a measurable requirement for focus appearance: an indicator at least as large as a 2 CSS pixel perimeter of the component, with a contrast ratio of at least 3:1 between the focused and unfocused states. That is criterion 2.4.13 Focus Appearance, and it is &lt;strong&gt;Level AAA&lt;/strong&gt;, not AA (&lt;a href="https://www.w3.org/TR/WCAG22/#focus-appearance" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;). The AA requirement is 2.4.7 Focus Visible, which asks only that the indicator be visible, so treat 2.4.13 as the bar to design to rather than as an AA failure in a client report. WCAG 2.2 also added &lt;strong&gt;2.4.11 Focus Not Obscured (Minimum)&lt;/strong&gt; at Level AA, which a sticky header or cookie banner covering the focused element will fail. For a sticky header, &lt;code&gt;scroll-padding-top&lt;/code&gt; set to the header's height keeps the focused element in view when the browser scrolls to it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/* Fixed: leave room for a 72px sticky header when scrolling to focus */
html { scroll-padding-top: 72px; }&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Skip links - bypassing repeated content&lt;/h2&gt;

&lt;p&gt;Imagine having to Tab through an entire navigation menu with 30 links on every single page before reaching the main content. For keyboard users, this is exhausting and time-consuming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip links&lt;/strong&gt; solve this by providing a link at the very top of the page that jumps directly to the main content area. It's typically hidden until it receives focus, so it doesn't affect the visual design:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Broken: the target cannot take focus, so the next Tab returns to the nav --&amp;gt;
&amp;lt;a href="#main"&amp;gt;Skip to content&amp;lt;/a&amp;gt;
...
&amp;lt;div id="main"&amp;gt;

&amp;lt;!-- Fixed: the target is a real landmark and can take focus --&amp;gt;
&amp;lt;a href="#main" class="skip-link"&amp;gt;Skip to content&amp;lt;/a&amp;gt;
...
&amp;lt;main id="main" tabindex="-1"&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The CSS hides it visually until focused:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.skip-link { position: absolute; top: -40px; left: 0; }
.skip-link:focus { top: 0; }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is a WCAG Level A requirement: 2.4.1 Bypass Blocks asks for "a mechanism" to "bypass blocks of content that are repeated on multiple web pages" (&lt;a href="https://www.w3.org/TR/WCAG22/#bypass-blocks" rel="noopener noreferrer"&gt;WCAG 2.2&lt;/a&gt;). It takes minimal effort to implement, and the &lt;code&gt;tabindex="-1"&lt;/code&gt; on the target is the detail most versions leave out.&lt;/p&gt;

&lt;h2&gt;Keyboard traps - the cardinal sin&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;keyboard trap&lt;/strong&gt; occurs when a user can Tab into an element but cannot Tab out of it. They're stuck, unable to navigate to other parts of the page without using a mouse - which they may not be able to do.&lt;/p&gt;

&lt;p&gt;Common sources of keyboard traps include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Embedded content&lt;/strong&gt; - iframes, embedded maps, and third-party widgets can trap focus inside their boundaries&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Custom rich text editors&lt;/strong&gt; - editors that capture Tab for indentation can prevent users from tabbing away&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Modal dialogs without proper focus management&lt;/strong&gt; - a modal should trap focus &lt;em&gt;within itself&lt;/em&gt; while open (this is correct behavior) but must release focus when closed&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Autofocusing scripts&lt;/strong&gt; - JavaScript that forces focus back to a specific element whenever the user tries to leave&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://getaccessguard.com/posts/understanding-wcag-the-standard-behind-accessible-websites" rel="noopener noreferrer"&gt;WCAG criterion&lt;/a&gt; 2.1.2 (No Keyboard Trap, Level A) prohibits this. When you build a component that handles Tab itself, such as an editor or a modal, give it a documented way out: Escape, or Tab once the user leaves the last field.&lt;/p&gt;

&lt;h2&gt;Making custom components keyboard accessible&lt;/h2&gt;

&lt;p&gt;Native HTML elements like &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; are keyboard accessible by default - they receive focus, respond to Enter/Space, and announce themselves to screen readers. Problems arise when developers build custom interactive elements from non-interactive HTML.&lt;/p&gt;

&lt;p&gt;A clickable &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a JavaScript click handler is invisible to keyboard users - it doesn't appear in the tab order, and pressing Enter does nothing. To make it accessible, you'd need to add:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;tabindex="0"&lt;/code&gt; - puts the element in the natural tab order&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;role="button"&lt;/code&gt; - tells assistive technologies it's a button&lt;/li&gt;
  &lt;li&gt;A keydown handler for Enter and Space&lt;/li&gt;
  &lt;li&gt;Proper focus styles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or you could just use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element and get all of this for free. The simplest rule for keyboard accessibility: &lt;strong&gt;use native HTML elements whenever possible&lt;/strong&gt;. A &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; instead of a &lt;code&gt;&amp;lt;div onclick&amp;gt;&lt;/code&gt;. An &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; instead of a &lt;code&gt;&amp;lt;span onclick&amp;gt;&lt;/code&gt;. Native elements handle keyboard interaction, focus management, and screen reader announcements automatically.&lt;/p&gt;

&lt;h2&gt;Testing keyboard accessibility&lt;/h2&gt;

&lt;p&gt;This guide is about building it. To check a finished page, follow our step by step &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand" rel="noopener noreferrer"&gt;manual keyboard accessibility test&lt;/a&gt;: what to press, what to look for at each stop and the WCAG criterion to write in the report.&lt;/p&gt;

&lt;h2&gt;How AccessGuard helps with keyboard accessibility&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://getaccessguard.com/checks/keyboard-and-focus" rel="noopener noreferrer"&gt;keyboard and focus checker&lt;/a&gt; looks for the causes a machine can find in the rendered page: divs and spans with click handlers that a keyboard cannot reach, &lt;code&gt;outline: none&lt;/code&gt; rules that leave no visible focus, sticky headers and banners that can cover the focused element, positive &lt;code&gt;tabindex&lt;/code&gt; values, and controls hidden with &lt;code&gt;opacity: 0&lt;/code&gt; that a keyboard still reaches.&lt;/p&gt;

&lt;p&gt;It cannot tell you whether the focus order makes sense, whether a widget traps focus or whether a menu works end to end, and no automated tool can confirm a site conforms to WCAG. Run a &lt;a href="https://getaccessguard.com/scan" rel="noopener noreferrer"&gt;free accessibility audit&lt;/a&gt; on a page to find the countable problems, then do the manual test for the rest.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>html</category>
    </item>
    <item>
      <title>Your sticky header hides the focused link. One CSS line fixes it.</title>
      <dc:creator>Denis Omerovic</dc:creator>
      <pubDate>Thu, 24 Sep 2026 07:47:17 +0000</pubDate>
      <link>https://dev.to/chille87/your-sticky-header-hides-the-focused-link-one-css-line-fixes-it-1la0</link>
      <guid>https://dev.to/chille87/your-sticky-header-hides-the-focused-link-one-css-line-fixes-it-1la0</guid>
      <description>&lt;p&gt;Press Tab on a page with a sticky header and keep going past the fold. Sooner or later the focused link scrolls into view &lt;em&gt;underneath&lt;/em&gt; the header. The keyboard user has no idea where they are.&lt;/p&gt;

&lt;p&gt;Since WCAG 2.2 this is an AA failure with its own number: &lt;strong&gt;2.4.11 Focus Not Obscured (Minimum)&lt;/strong&gt;. W3C's words: "When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content." The usual causes are named in the &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html" rel="noopener noreferrer"&gt;Understanding document&lt;/a&gt;: "sticky footers, sticky headers, and non-modal dialogs", and a cookie banner "will fail this success criterion if it entirely obscures a component receiving focus".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;When an element gets focus off screen, the browser scrolls just far enough to bring it into the viewport. It knows nothing about your &lt;code&gt;position: sticky&lt;/code&gt; or &lt;code&gt;position: fixed&lt;/code&gt; header, so it happily parks the element at the very top, behind the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Tell the scroll container how much of it is covered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--header-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scroll-padding-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--header-height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.site-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--header-height&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;scroll-padding&lt;/code&gt; defines an inset that scrolling into view respects, so the focused element stops below the header instead of under it. The same property fixes in-page anchor links (&lt;code&gt;#section&lt;/code&gt;) landing under the header, so you get two bugs for one line.&lt;/p&gt;

&lt;p&gt;A cookie banner or chat button fixed to the bottom needs the other side too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scroll-padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* height of the bottom banner */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the scrolling happens inside a panel rather than the page, put &lt;code&gt;scroll-padding&lt;/code&gt; on that panel, since it applies to the element that scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it in 30 seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Load the page fresh, with the cookie banner still showing.&lt;/li&gt;
&lt;li&gt;Click the address bar, then press Tab until focus is past the first screen.&lt;/li&gt;
&lt;li&gt;Every focused element should be at least partly visible. Minimum means not &lt;em&gt;entirely&lt;/em&gt; hidden, but aim for fully visible, which is what 2.4.12 asks for at AAA.&lt;/li&gt;
&lt;li&gt;Press Shift + Tab back up, because upward scrolling can hide things under a bottom banner.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automated scanners mostly can't catch this, because it depends on scroll position and on which element has focus at that moment. It is one of the nine steps in a manual keyboard pass: &lt;a href="https://getaccessguard.com/posts/how-to-test-keyboard-accessibility-by-hand?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=guide-keyboard-testing&amp;amp;utm_content=brand-article" rel="noopener noreferrer"&gt;how to test keyboard accessibility by hand&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
