<?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: AgentKit</title>
    <description>The latest articles on DEV Community by AgentKit (@agentkit).</description>
    <link>https://dev.to/agentkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3836301%2F97b1a04a-6836-4c3d-abd0-be0c97d58d56.png</url>
      <title>DEV Community: AgentKit</title>
      <link>https://dev.to/agentkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agentkit"/>
    <language>en</language>
    <item>
      <title>Countdown Timer Accessibility: Why Your Sale Widget Fails Screen Readers</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sun, 10 May 2026 05:00:34 +0000</pubDate>
      <link>https://dev.to/agentkit/countdown-timer-accessibility-why-your-sale-widget-fails-screen-readers-1hc4</link>
      <guid>https://dev.to/agentkit/countdown-timer-accessibility-why-your-sale-widget-fails-screen-readers-1hc4</guid>
      <description>&lt;p&gt;Walk through a dozen Shopify or WooCommerce stores on a Saturday morning and you will see the same thing on most of them: a bright bar across the top of the screen counting down to the end of a sale. Two days, eleven hours, forty-two minutes, eighteen seconds. Seventeen. Sixteen.&lt;/p&gt;

&lt;p&gt;For a sighted shopper this is just visual noise. You glance, you understand, you keep scrolling. For a screen reader user, a low-vision user with screen magnification, or a customer with a cognitive disability, that same widget is one of the most disruptive elements on the page. In some cases it makes the rest of the site unusable. In a few of the audits I have run this year, the countdown timer was the single biggest accessibility issue on an otherwise reasonable store -- and the owner had no idea, because they had never tested the page with the assistive technology that real customers use.&lt;/p&gt;

&lt;p&gt;Memorial Day, Father's Day, the summer sale season, Prime Day, Black Friday. The next six months are the heaviest countdown-timer season of the year. If you are running an ecommerce site, this is worth thirty minutes of your time before the next promotion ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  What screen reader users actually hear
&lt;/h2&gt;

&lt;p&gt;Screen readers read the page out loud. The exact behaviour depends on which screen reader and which browser, but the way most countdown widgets are built today, the experience falls into one of three categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "constant interruption" pattern.&lt;/strong&gt; The timer is wrapped in an element with &lt;code&gt;aria-live="polite"&lt;/code&gt; or &lt;code&gt;aria-live="assertive"&lt;/code&gt;, which is supposed to announce updates as they happen. The developer who installed it thought this was the accessible thing to do. In practice the browser fires an announcement every single second -- "two days eleven hours forty-two minutes seventeen seconds" -- and then again the next second, and the next. A screen reader user trying to read the product description gets every other word drowned out by a stopwatch reading itself out loud, forever, until they manage to navigate away from the page or turn off live regions globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "invisible to assistive tech" pattern.&lt;/strong&gt; The timer is implemented with CSS pseudo-elements (&lt;code&gt;::before&lt;/code&gt; content), background images, or canvas drawing, none of which screen readers can access. A blind user has no idea the sale is ending in two days. They miss the urgency cue entirely, which is bad for them and bad for your conversion rate. If the urgency framing is what convinces a customer to buy, you are excluding a meaningful slice of your audience from the offer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "untrusted content" pattern.&lt;/strong&gt; The timer is real DOM text but it is positioned &lt;code&gt;fixed&lt;/code&gt; to the bottom of the screen, overlapping product photos and the add-to-cart button. On a phone in landscape it covers half the screen. Users with screen magnification cannot dismiss it. Users on small viewports cannot scroll past it. This is not strictly a screen reader issue, but it is the same family of problem -- a widget designed for one persona that breaks for everyone else.&lt;/p&gt;

&lt;p&gt;All three patterns appear regularly in 2026 store audits. All three are easy to fix. Most store owners have never been told they exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WCAG criteria a bad countdown timer breaks
&lt;/h2&gt;

&lt;p&gt;If you have to justify the work to a client or a boss, here are the specific WCAG 2.2 success criteria a typical countdown timer fails. None of these are obscure -- all four are Level A or AA, which are the levels referenced by the European Accessibility Act, the Americans with Disabilities Act case law, and the Section 508 procurement standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.2.2 Pause, Stop, Hide (Level A).&lt;/strong&gt; Any moving, blinking, scrolling, or auto-updating content that lasts longer than five seconds must give the user a way to pause it, stop it, or hide it. A countdown that ticks every second for hours has no off switch on most ecommerce stores I audit. That fails 2.2.2 outright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.1.3 Status Messages (Level AA).&lt;/strong&gt; Status messages that communicate change should be programmatically announced without taking focus from the user. The "constant interruption" pattern above takes the spirit of 4.1.3 and weaponises it -- the developer knew enough to add an &lt;code&gt;aria-live&lt;/code&gt; region but did not understand that announcing every tick of the clock is exactly the failure mode the criterion is trying to prevent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.4.10 Reflow (Level AA).&lt;/strong&gt; Content must reflow at 320 pixels wide without losing functionality. A countdown bar that uses absolute or fixed positioning often covers controls or pushes critical content off screen at small viewports. This is one of the most-cited issues in mobile accessibility audits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.4.13 Content on Hover or Focus (Level AA).&lt;/strong&gt; Tooltip-style "ends in" overlays that appear on hover or focus must be dismissable without moving the pointer or focus. Many sale-popup timers fail this -- you hover over the icon, the timer fills the screen, and the only way to make it go away is to refresh the page.&lt;/p&gt;

&lt;p&gt;You do not need to memorise these. You do need to understand that there is real, reasonably specific guidance for what to do, and that "we used a popular plugin" is not a defence in a demand letter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test your own countdown widget in five minutes
&lt;/h2&gt;

&lt;p&gt;You do not need a developer to do this. You do not need to install anything if you are on a Mac or a Windows machine with a screen reader already installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a Mac:&lt;/strong&gt; open Safari, navigate to your storefront, and press Command + F5 to turn on VoiceOver. Press Control + Option + A to start reading the page from the top. Listen for ten seconds. If you hear the timer announced over and over while VoiceOver is trying to read the rest of the page, you have the constant-interruption problem. If you hear nothing about a sale at all but you can see one on screen, you have the invisible-to-assistive-tech problem. Press Command + F5 again to turn VoiceOver off when you are done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a Windows machine:&lt;/strong&gt; download the free &lt;a href="https://www.nvaccess.org/" rel="noopener noreferrer"&gt;NVDA screen reader&lt;/a&gt;, install it, and open Chrome. Press Insert + Down Arrow to start reading. Same listening exercise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On any device:&lt;/strong&gt; open your storefront, press Tab repeatedly with the keyboard only. If the countdown widget includes any interactive element (a "shop now" button, a close button, a "view details" link), it should appear in the tab order with a visible focus indicator. If it does not, it is failing keyboard users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five minutes total.&lt;/strong&gt; You will know more about how disabled customers experience your store than the agency that built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ship instead
&lt;/h2&gt;

&lt;p&gt;You do not have to remove urgency from your storefront. You do have to communicate it in a way that does not actively harm users on assistive technology. Three patterns that work in 2026:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static end-of-sale text, not a ticking clock.&lt;/strong&gt; "Sale ends Monday at midnight Eastern" is just as clear as a ticking countdown for almost every conversion-relevant decision. It does not announce itself every second, it does not require any clever ARIA, and it reflows perfectly. The only thing it loses is the visual drama of watching the clock tick. If your conversion lift from urgency depends on watching a clock tick, you have a different problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A countdown that updates in larger increments.&lt;/strong&gt; If you must show live time, update only every minute (or every fifteen minutes if the sale lasts days), and use an &lt;code&gt;aria-live="polite"&lt;/code&gt; region that contains only the new value, not the full timer. A screen reader user will hear "one hour fifteen minutes remaining" once a minute, which is informative without being maddening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A countdown that is purely visual, with a screen-reader-only equivalent.&lt;/strong&gt; Mark the visual ticking timer as &lt;code&gt;aria-hidden="true"&lt;/code&gt; so assistive technology ignores it entirely, and put a visually-hidden static text element nearby that says "Sale ends Monday May 26 at 11:59 PM Eastern." Sighted users see the urgency, screen reader users get the same information without the noise.&lt;/p&gt;

&lt;p&gt;Whichever pattern you choose, add a real close button with a visible focus state and a meaningful accessible name (not just an "X" character with no &lt;code&gt;aria-label&lt;/code&gt;). Test at 320 pixel viewport width to confirm the bar does not overlap critical controls. If you are using a third-party app or plugin, check the developer's accessibility statement before installing -- and if there isn't one, that tells you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform-specific notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shopify.&lt;/strong&gt; Most popular sale-app countdown timers (Hextom, Essential, Booster) install with the constant-interruption or invisible-to-assistive-tech pattern out of the box. The good news is that these apps usually support custom CSS and a "static text" mode hidden in their settings -- look for a toggle labelled something like "show end date instead of timer" or "compact mode". If the app you use does not have one, file a feature request and use a different app for the next promotion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WooCommerce.&lt;/strong&gt; Most countdown plugins (YITH, FuseWP, RWP) inject HTML directly into the theme. You can usually edit the template file in the plugin's settings to add &lt;code&gt;aria-hidden="true"&lt;/code&gt; to the visual countdown wrapper and a visually-hidden static text alternative. If your developer is involved at all, this is a fifteen-minute fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Squarespace and Wix.&lt;/strong&gt; The built-in countdown blocks are better than most third-party apps but still default to live ticking with &lt;code&gt;aria-live&lt;/code&gt;. Hide the live region with custom CSS and add a static text block underneath for the same effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom builds.&lt;/strong&gt; If your storefront was custom-built, ask the developer to confirm that the countdown widget passes 2.2.2, 4.1.3, 1.4.10, and 1.4.13. If they do not know what those mean, that is a useful signal about how much accessibility work is happening on your site in general.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if you cannot change it before the next sale
&lt;/h2&gt;

&lt;p&gt;You ship the promotion as-is, and you publish a short note in your accessibility statement acknowledging the issue. Something like: "Our current sale-period countdown widget may interrupt screen reader users. We are working with our platform vendor to replace it. In the meantime, please contact us at [email] for sale information in an alternative format." That note does not make you compliant, but it does demonstrate good faith, which matters in demand-letter triage and in serious-claim defence.&lt;/p&gt;

&lt;p&gt;Then put a calendar reminder for the week after the sale to fix it for real. Most ecommerce store owners I have worked with discover that the fix takes one short conversation with their developer or app vendor, not a months-long project. The reason it has not been done is not that it is hard. It is that no one has ever pointed out the problem.&lt;/p&gt;

&lt;p&gt;Now you have. The next promotion is the right time to ship the fix.&lt;/p&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/accessible-modals-popups-guide/"&gt;Accessible Modals and Popups: A Practical Guide&lt;/a&gt; -- the same family of overlays as countdown bars, with similar fixes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/chatbot-live-chat-accessibility/"&gt;Why Your Live Chat Widget Is Failing Screen Reader Users&lt;/a&gt; -- another widget that screen reader users disproportionately struggle with&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/accessible-ecommerce-checkout-guide/"&gt;Accessible Ecommerce Checkout: A Step-by-Step Guide&lt;/a&gt; -- the next thing to fix once the countdown bar is sorted&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PostToolUse Hooks for Audit Logs: A Production Pattern with Code</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sat, 09 May 2026 07:47:32 +0000</pubDate>
      <link>https://dev.to/agentkit/posttooluse-hooks-for-audit-logs-a-production-pattern-with-code-39n2</link>
      <guid>https://dev.to/agentkit/posttooluse-hooks-for-audit-logs-a-production-pattern-with-code-39n2</guid>
      <description>&lt;p&gt;If your team is running Claude Code in production, you can probably tell me what it can do. The harder question is what it actually did last Tuesday at 3pm — which Bash calls ran, against which repo, with what exit code. The &lt;code&gt;PostToolUse&lt;/code&gt; hook is the lifecycle event you have to use, and most documentation skips past it in two paragraphs.&lt;/p&gt;

&lt;p&gt;The previous article in this series treated &lt;code&gt;PreToolUse&lt;/code&gt; as a tiny state machine — a gate that decides what Claude Code is allowed to do next. &lt;code&gt;PostToolUse&lt;/code&gt; is the other half of that pair. It runs after the tool call has already happened, and the only useful thing it can do is record. Everything we describe below is built around that one premise, because the moment you forget it, the hook stops being an audit log and becomes a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a PostToolUse hook
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook is a gate. It runs before the tool call, and its exit code decides whether the call goes through. A &lt;code&gt;PostToolUse&lt;/code&gt; hook is a recorder. It runs after the call, sees the result, and writes a line somewhere durable. The two hooks share a payload shape but they answer different questions: "should this happen" versus "what just happened."&lt;/p&gt;

&lt;p&gt;For compliance work, the second question is the one that matters. SOC 2 CC6.1 and CC7.2 controls, the kind teams typically point to under access logging and system monitoring, are about retroactive evidence — "show us what your privileged operators did over the last ninety days." Claude Code in agentic mode is, by any reasonable reading of those controls, a privileged operator running on your engineer's machine. Whatever your interpretation of the control text, the answer "we will check the model's session transcript" is not going to land well with an auditor.&lt;/p&gt;

&lt;p&gt;The official Claude Code documentation describes &lt;code&gt;PostToolUse&lt;/code&gt; in roughly two paragraphs and a JSON schema. That is enough to get a hook running. It is not enough to get one that survives a compliance review. The rest of this article fills that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hook that does the basics
&lt;/h2&gt;

&lt;p&gt;Here is the core of a &lt;code&gt;PostToolUse&lt;/code&gt; audit hook. The version below is the one we are putting into our own Claude Code Max setup, designed against the failure modes we have seen in adjacent hook code over the last few months.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# PostToolUse: append every tool call as a JSON line.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;      | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_name // "unknown"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;       | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;EXIT_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_response.exit_code // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/audit
jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; ts &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; tool &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--argjson&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{ts: $ts, tool: $tool, command: $command, exit_code: $exit}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.claude/audit/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;.jsonl

&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things in this snippet are not accidental. The timestamp is UTC ISO-8601, because by the time you are reading the log six weeks later you do not want to be guessing what timezone the engineer's laptop was in. The output format is JSON Lines (one event per line, no commas, no wrapping array), because it is append-friendly, &lt;code&gt;grep&lt;/code&gt;-friendly, and trivially streamable into anything that wants line-delimited JSON. The file rotates daily, because monthly files become unwieldy past 50 MB and hourly files explode in count. The exit code is &lt;code&gt;0&lt;/code&gt; regardless of whether the write succeeded — we will come back to why that matters.&lt;/p&gt;

&lt;p&gt;Twelve lines of substance is enough to start. We have not yet seen a team that needed more than this in the first month; what they needed was the discipline to actually keep the log, route it somewhere durable, and read it before an auditor asked. The hook itself is the cheap part.&lt;/p&gt;

&lt;h2&gt;
  
  
  PII redaction is a hook responsibility
&lt;/h2&gt;

&lt;p&gt;An audit log is a piece of information you have promised yourself you will keep. That is also exactly what makes it dangerous. Every secret your engineer accidentally types into a Bash call is now sitting in &lt;code&gt;~/.claude/audit/&lt;/code&gt; waiting for the next backup, the next disk image, the next stolen laptop. Data minimization is one of the principles in GDPR Article 5; whatever your interpretation of it, redaction at the hook layer is the cheapest place to apply it.&lt;/p&gt;

&lt;p&gt;The pattern that has held up for us is a two-pass &lt;code&gt;sed&lt;/code&gt; replacement, applied to the command field before it reaches the JSON line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CMD_SAFE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD_RAW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/(Bearer |Token |key=|password=|secret=)[A-Za-z0-9_\.\-]+/\1&amp;lt;REDACTED&amp;gt;/gi'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/(ghp_|gho_|ghs_|ghu_|ghr_)[A-Za-z0-9]{20,}/\1&amp;lt;REDACTED&amp;gt;/g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/(sk-ant-|sk-)[A-Za-z0-9_\-]{20,}/\1&amp;lt;REDACTED&amp;gt;/g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/(AKIA)[A-Z0-9]{16}/\1&amp;lt;REDACTED&amp;gt;/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches &lt;code&gt;Bearer&lt;/code&gt;-style headers, &lt;code&gt;key=&lt;/code&gt; and &lt;code&gt;password=&lt;/code&gt; query parameters, GitHub personal access tokens by prefix, OpenAI and Anthropic keys by prefix, and AWS access keys by their &lt;code&gt;AKIA&lt;/code&gt; pattern. A line in the resulting log looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-04-30T13:42:01Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"curl -H 'Authorization: Bearer &amp;lt;REDACTED&amp;gt;' https://api.github.com/user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"exit_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest part is that this is a deny-list. New token formats — a new prefix from a new vendor, a custom in-house signing scheme, anything you have not added a regex for — will pass through unredacted until somebody updates the pattern. Two failure modes have shown up in our own usage. The first is the unknown-prefix problem above, which only an allow-list approach truly fixes (record only the command name, drop the arguments entirely; lose searchability, gain safety). The second is multi-arg tokens with embedded spaces, where the boundary the regex is looking for never appears; we have not yet seen this in the wild on a standard Claude Code Bash session, but custom CLIs that take credentials as positional arguments make it plausible.&lt;/p&gt;

&lt;p&gt;If your environment is one where "best effort" is not enough, the right move is not to write a smarter regex. The right move is to record less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this log goes
&lt;/h2&gt;

&lt;p&gt;Once the hook is producing JSON lines, the next decision is where they live. The four destinations production teams actually pick are local files, syslog, a managed logging service, and an append-only object store. Each one is fine for a different size and shape of team, and the difference is mostly about retention guarantees and what your auditor wants to see.&lt;/p&gt;

&lt;p&gt;A local file at &lt;code&gt;~/.claude/audit/&lt;/code&gt; is what the snippet above writes to. It is free, it is fast, and it survives precisely as long as the laptop survives. For a single developer experimenting with hooks, that is enough. For a team, it is the destination you ship from, not the destination you ship to.&lt;/p&gt;

&lt;p&gt;Syslog (&lt;code&gt;logger -t claude-code&lt;/code&gt; piped from the same hook) gets you onto OS-standard logging infrastructure, which already has retention rules, rotation, and probably a fleet log shipper attached to it. The retention guarantee is whatever the OS is configured for, which means it is whatever your platform team has decided, which means you should ask them before assuming. We have seen this destination work well for small engineering teams whose platform group already has a logging pipeline.&lt;/p&gt;

&lt;p&gt;A managed logging service — Datadog, Honeycomb, CloudWatch, the rest — is where most enterprise teams end up. The hook becomes a curl into the service's intake, or the file gets tailed by the service's agent. Cost scales with volume. Retention is configurable. The compliance fit is generally good, because these services advertise their controls precisely so that compliance teams can read them.&lt;/p&gt;

&lt;p&gt;An append-only object store with immutability guarantees — S3 with Object Lock, GCS with retention policies — is the destination teams pick when their auditor asks specifically about tamper-resistance. The hook does not write directly there; a daemon ships the rotated daily files in. This is where the audit log graduates from "we keep records" to "we can prove we kept records."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Immutability&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Compliance fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/audit/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OS-dependent&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Dev / staging only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syslog&lt;/td&gt;
&lt;td&gt;OS configuration&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Small team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datadog / Honeycomb&lt;/td&gt;
&lt;td&gt;Configurable&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Volume-based&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 + Object Lock&lt;/td&gt;
&lt;td&gt;Effectively infinite&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Storage-based&lt;/td&gt;
&lt;td&gt;SOC 2 / SOX evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading the table is less interesting than the reason behind the picks. Teams move from row one to row four roughly in step with how seriously they are taking compliance, and roughly in step with how often the audit log gets read by someone who is not the engineer who wrote it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does not belong in this hook
&lt;/h2&gt;

&lt;p&gt;This is the section where we want to say something specific about scope, because audit infrastructure has a well-known failure mode and there is no reason a Claude Code hook would be exempt. An audit hook starts as twelve lines that record an event. Then somebody adds an alert when a particular command runs. Then somebody adds a filter to drop noisy commands. Then somebody adds an aggregation step that batches calls before writing. Six months later, the hook is two hundred lines, takes longer to run than the actual tool call, and is the thing that breaks when the next Claude Code release changes a payload field.&lt;/p&gt;

&lt;p&gt;Alerting logic, filtering by command type, realtime aggregation — none of these belong in the hook itself. The hook records the event. A separate process — a log tailer, a scheduled query, an alerting rule on the centralized log destination — reads the event and decides whether to do something about it. The separation matters because hooks degrade and logs accumulate, and the cost of mixing the two responsibilities shows up later, when the system is harder to fix.&lt;/p&gt;

&lt;p&gt;The shape we want to defend is one where the hook is the cheapest piece of code in the audit pipeline, and any time it accumulates "just one more responsibility" it becomes the most expensive. Keep the hook focused: write the event, redact the obvious secrets, exit zero, get out of the session's way.&lt;/p&gt;

&lt;p&gt;That last point — exit zero — is also why the snippet above sets &lt;code&gt;set -uo pipefail&lt;/code&gt; instead of &lt;code&gt;set -e&lt;/code&gt;, and why every internal failure path falls back to a stderr warning rather than a non-zero exit. &lt;code&gt;PostToolUse&lt;/code&gt; runs after the tool has already executed. There is no version of "the hook failed, so the call should not have happened" that makes sense at this point in the lifecycle. A non-zero exit from a &lt;code&gt;PostToolUse&lt;/code&gt; hook propagates as a hook failure into the session, which is the wrong response when the only thing the hook does is record. Fail open with a visible warning. Never fail closed in silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-week checklist
&lt;/h2&gt;

&lt;p&gt;If you put this hook in place today, here is what we recommend you check before two weeks pass: are the timestamps you are actually getting in UTC, or did somebody's laptop quietly drift; is the file size growing at the rate you predicted, or is it ten times bigger because some background tool is calling Bash on a loop; have you tested the redaction against a real command that contained a real-shaped token, not just the placeholder you used to write the regex; can your compliance lead read one day's log without engineering help, or does the schema need a field renamed before it makes sense to anyone outside the team that built it. We have not yet seen a team that got all four right on the first try, and the order in which they fail is roughly the order we just listed them in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this hook earns its keep
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;PostToolUse&lt;/code&gt; hook is the cheapest piece of compliance infrastructure available to a team running Claude Code. The cost of writing it is a dozen lines of Bash, a regex you can copy, and a directory you can create with &lt;code&gt;mkdir -p&lt;/code&gt;. The cost of not writing it is the inability to answer "what did the agent do" six weeks after the fact, which is precisely the question that gets asked when something has gone wrong and somebody needs an answer in writing.&lt;/p&gt;

&lt;p&gt;Most documentation treats &lt;code&gt;PostToolUse&lt;/code&gt; as a logging convenience. The teams we have seen scale Claude Code beyond a single developer treat it as the audit trail. The framing matters because it changes which destination you pick, how much energy you spend on redaction, and whether the hook ends up living in a &lt;code&gt;notes/&lt;/code&gt; folder or in a settings file that is reviewed every time somebody onboards.&lt;/p&gt;

&lt;p&gt;Not legal advice. We describe own usage and patterns we have observed; the way these patterns map onto SOC 2, SOX, HIPAA, or GDPR in your specific environment is a question for your compliance team.&lt;/p&gt;




&lt;p&gt;We are open-sourcing the AgentKit Hooks Pack — production-ready templates for &lt;code&gt;PreToolUse&lt;/code&gt; permission gating, &lt;code&gt;PostToolUse&lt;/code&gt; audit logs, kill switch sentinels, notification routing — under Apache 2.0 in late May. The day it lands, we email a launch note and the Companion Guide PDF (sixty pages on lifecycle events, failure modes, and rollout patterns) to the pre-launch list. To get on it: &lt;a href="https://imta71770-dot.github.io/agentkit-hooks-pack/" rel="noopener noreferrer"&gt;imta71770-dot.github.io/agentkit-hooks-pack&lt;/a&gt;. The repo with templates and license is at &lt;a href="https://github.com/imta71770-dot/agentkit-hooks-pack" rel="noopener noreferrer"&gt;github.com/imta71770-dot/agentkit-hooks-pack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>devtools</category>
      <category>compliance</category>
      <category>ai</category>
    </item>
    <item>
      <title>Mother's Day Flower Orders: 6 Accessibility Failures Costing Florists Sales This Weekend</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sat, 09 May 2026 05:00:38 +0000</pubDate>
      <link>https://dev.to/agentkit/mothers-day-flower-orders-6-accessibility-failures-costing-florists-sales-this-weekend-dk6</link>
      <guid>https://dev.to/agentkit/mothers-day-flower-orders-6-accessibility-failures-costing-florists-sales-this-weekend-dk6</guid>
      <description>&lt;p&gt;Mother's Day is the single biggest sales day of the year for most florist websites. In the U.S. it routinely outsells Valentine's Day, and the order window is shockingly short — most customers buy between Wednesday and Saturday afternoon, with a long tail of last-minute orders placed Sunday morning.&lt;/p&gt;

&lt;p&gt;If your site has accessibility problems, this is the weekend they cost you the most money. A blind grandparent trying to send flowers to their daughter, a person with motor impairments scheduling a Sunday-morning surprise delivery, an elderly father with low vision picking out a bouquet — these are some of the most motivated customers you have. They have already decided to buy. They just need a website that doesn't fight them.&lt;/p&gt;

&lt;p&gt;Here are six accessibility failures we see again and again on florist sites this week, and the plain-English fixes you can ship in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Product photos with empty or generic alt text
&lt;/h2&gt;

&lt;p&gt;This is the single most common — and most expensive — defect on florist websites. You have a beautiful catalog of arrangements: tall vase, hand-tied bouquet, sympathy basket, garden-style centerpiece. Each one is presented as a photo with the alt text empty, blank, or set to something useless like &lt;code&gt;flowers.jpg&lt;/code&gt;, &lt;code&gt;arrangement&lt;/code&gt;, or &lt;code&gt;IMG_3492&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A customer using a screen reader hears "image, image, image, image" and cannot tell what they are looking at. They can't compare arrangements. They can't pick the one for Mom. They leave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; Open your product catalog in your storefront admin (Shopify, Squarespace, BloomNation, FloristWare, Lovingly, whatever you use) and add a real description to every "alt text" or "image description" field. Write what the customer would say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Round arrangement in clear glass cube vase with white roses, pink peonies, and eucalyptus greenery, approximately 10 inches tall and 10 inches wide. Suitable for Mother's Day."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. No special skills required. If you only have time to do twenty arrangements before Sunday, do your top twenty Mother's Day sellers first.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Same-day-delivery date pickers that won't accept a keyboard
&lt;/h2&gt;

&lt;p&gt;Most florist sites have a custom calendar widget for picking the delivery date. The customer is supposed to click a date with their mouse. Try this instead: open your own site in a browser, click on the delivery date field, and try to use only the Tab and arrow keys to pick a date. Most of the time, nothing happens. The keyboard is locked out entirely.&lt;/p&gt;

&lt;p&gt;This is a problem for customers with motor impairments who cannot use a mouse, customers using switch devices, screen-reader users, and anyone whose mouse just died. On a holiday weekend, that's not a small group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; Talk to your website vendor — or, if you self-host on Shopify or WordPress, swap the custom calendar widget for a plain HTML date field (&lt;code&gt;&amp;lt;input type="date"&amp;gt;&lt;/code&gt;). It looks slightly less stylish, but it works for everyone, on every device, with every assistive technology, with zero JavaScript. Most major florist platforms (Lovingly, BloomNation, FloristWare) have a setting to use the native date picker — turn it on.&lt;/p&gt;

&lt;p&gt;If you cannot ship that change before Sunday, do this instead: at the very top of your delivery page, add a clearly visible phone number with the words &lt;strong&gt;"Can't use the date picker? Call us at [your number] and we'll take your order on the phone."&lt;/strong&gt; Make sure someone is actually answering that phone Saturday and Sunday.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Delivery-time-window selectors that hide all the information in color
&lt;/h2&gt;

&lt;p&gt;Most sites offer time windows: "Today by 2 PM," "Today after 4 PM," "Tomorrow morning." The standard implementation is a row of colored buttons — green for available, gray for unavailable, red for "fully booked." The text labels are tiny or absent.&lt;/p&gt;

&lt;p&gt;If you can't see the colors clearly, you have no idea which slots are open. People with color-blindness, low vision, or aging eyes simply cannot tell. They guess, hit a fully-booked slot, get an error, and leave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; Make sure every time-window button has a clear text label that says what it actually means: &lt;strong&gt;"Today by 2:00 PM — order in the next 90 minutes,"&lt;/strong&gt; &lt;strong&gt;"Today after 4:00 PM — fully booked,"&lt;/strong&gt; &lt;strong&gt;"Sunday morning — available."&lt;/strong&gt; The customer should be able to read the page in pure black-and-white and still understand which slots are available.&lt;/p&gt;

&lt;p&gt;If you can't change the buttons themselves, add a heading above them: &lt;strong&gt;"All slots shown are currently available. Sold-out slots have been removed."&lt;/strong&gt; Then make sure your booking system actually removes sold-out slots instead of greying them out.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Funeral-home and hospital-delivery lookups that don't work without a mouse
&lt;/h2&gt;

&lt;p&gt;Sympathy and hospital deliveries are 25–40 percent of florist revenue, but the funeral-home lookup widget is one of the most consistently inaccessible features on the entire site. The customer types in a city or ZIP code, a list of funeral homes drops down, and they're supposed to click one.&lt;/p&gt;

&lt;p&gt;Try selecting a funeral home with only the keyboard. On most sites, you can't. The dropdown doesn't respond to arrow keys. You can't hear which funeral home is highlighted. You give up and pick the wrong one. The flowers go to the wrong service. The customer is mortified, you have to refund and re-send, and you've lost both the customer and the money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; This one is harder to fix without your developer or vendor's help, but you can put a clear fallback in place today. Right above the funeral-home search field, add a notice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Don't see the funeral home you need, or having trouble with the search? Call us at [your number] — we deliver to every funeral home in our service area, and we'll confirm the address with you on the phone before we send the driver."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Make this visible. Don't bury it in the FAQ. A grieving customer should not have to hunt for the rescue path.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Variation pickers (Standard / Deluxe / Premium) with no description of what changes
&lt;/h2&gt;

&lt;p&gt;Almost every florist site offers each arrangement in three sizes: Standard, Deluxe, Premium. The price difference is significant — often $20–$40 between Standard and Premium. But the page rarely tells the customer what they're paying for. The Standard photo is just a slightly smaller version of the Deluxe photo. There's no text explanation.&lt;/p&gt;

&lt;p&gt;A sighted customer can squint at the photos and guess. A blind customer using a screen reader hears "Standard, Deluxe, Premium" and has no information at all. They pick the cheapest option, or they leave because they can't tell whether Premium is worth twice the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; For each variation, add a one-sentence description directly under the option label. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard ($79):&lt;/strong&gt; Approximately 10 stems in a 6-inch glass vase. Best for desktops or small side tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deluxe ($109):&lt;/strong&gt; Approximately 18 stems in an 8-inch glass vase, with additional roses and eucalyptus. Best for kitchen counters or dining tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium ($149):&lt;/strong&gt; Approximately 28 stems in a 10-inch glass vase, with peonies, lisianthus, and seasonal accent flowers added. Best for living-room centerpieces or as a "wow" gift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This single change typically lifts conversion on premium tiers by 10–25 percent — not just for accessibility, but for everyone. Customers buy more when they understand what they're buying.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The post-purchase confirmation that disappears in three seconds
&lt;/h2&gt;

&lt;p&gt;The customer fills out the form, enters their card, hits submit — and a small green "Order placed!" banner appears at the top of the page for three seconds, then vanishes. No confirmation page. No order number on screen. The email confirmation will arrive in twenty minutes (or, on a busy holiday weekend, an hour).&lt;/p&gt;

&lt;p&gt;Sighted customers see the green banner and have a vague sense the order went through. Screen-reader users may not hear it announced at all if it's implemented as a transient toast. Customers with cognitive disabilities, low vision, or anxiety lose the feedback they need to be sure the order happened. They reload the page, the cart is empty, the banner is gone, and now they're not sure if they paid once, twice, or zero times. They call your shop in a panic on Saturday afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it this afternoon:&lt;/strong&gt; Make sure your checkout flow ends on a real, persistent confirmation page with the order number, the recipient's name, the delivery date and window, and a clear instruction: &lt;strong&gt;"You'll receive a confirmation email within 5 minutes. If you don't, please call us at [phone]."&lt;/strong&gt; The page should not auto-redirect, auto-close, or rely on a banner that fades out.&lt;/p&gt;

&lt;p&gt;If your checkout platform doesn't support a persistent confirmation page, at minimum lengthen the success banner to stay visible for at least 20 seconds and add an order number to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 60-minute pre-Mother's-Day audit checklist
&lt;/h2&gt;

&lt;p&gt;If you've got an hour this afternoon, walk through your site as a customer would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your homepage in a browser. Press Tab repeatedly. Can you reach every link, button, and form field? Can you see where the focus is at every step?&lt;/li&gt;
&lt;li&gt;Pick a Mother's Day arrangement. Read the alt text on the product photo. Does it actually describe the arrangement?&lt;/li&gt;
&lt;li&gt;Read the variation descriptions for Standard / Deluxe / Premium. Can a customer who has never seen the photo tell what they're getting?&lt;/li&gt;
&lt;li&gt;Try to schedule a same-day delivery using only the keyboard. Can you?&lt;/li&gt;
&lt;li&gt;Check the time-window selector. Does it tell you in plain text which windows are available, or only with color?&lt;/li&gt;
&lt;li&gt;Complete a test order. Does the confirmation stay on screen?&lt;/li&gt;
&lt;li&gt;Look at your sympathy and hospital-delivery flows. Is there a clear "call us" fallback for customers who can't use the search?&lt;/li&gt;
&lt;li&gt;Make sure your phone number is staffed Saturday and Sunday — and that the person answering is empowered to take a complete order over the phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will not get to perfect accessibility before Sunday. Nobody does. But the six fixes above are the difference between a customer who leaves your site frustrated and a customer who hits "Place Order." On the highest-revenue weekend of the year, that's the difference between a great Mother's Day and a disappointing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Mother's Day: the legal picture
&lt;/h2&gt;

&lt;p&gt;Florists have been one of the most-targeted small-business categories for accessibility lawsuits since 2023. California Unruh statutory damages are $4,000 per visit. New York State and New York City attorneys file in waves of 30–80 cases per quarter, often timed to land just after major holidays. Florida and Pennsylvania settlements typically run $5,000–$20,000 plus remediation costs.&lt;/p&gt;

&lt;p&gt;The European Accessibility Act, which took effect on June 28, 2025, explicitly covers consumer e-commerce. If you take orders from customers in any EU member state — including for cross-border deliveries via Teleflora, Interflora, or Euroflorist — your storefront is in scope and member-state regulators can fine non-conforming services up to €1 million.&lt;/p&gt;

&lt;p&gt;None of that is a reason to panic. It is a reason to start somewhere — and starting with the six fixes above puts you in materially better shape than 90 percent of florist sites we audit. The rest can come in the weeks after Mother's Day, when you have time to breathe.&lt;/p&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers — no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/alt-text-guide/"&gt;Alt Text Guide: Writing Image Descriptions That Work&lt;/a&gt; — the long-form companion to fix #1, with examples for every product type.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/accessible-ecommerce-checkout-guide/"&gt;Accessible E-commerce Checkout Guide&lt;/a&gt; — covers the date pickers, payment forms, and confirmation flows from fixes #2, #3, and #6.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/ada-lawsuits-small-business/"&gt;ADA Lawsuits and Small Businesses: What Actually Happens&lt;/a&gt; — what an accessibility complaint looks like in practice, and how florist-sized businesses typically respond.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hiding Underlines on Hyperlinks: The Accessibility Bug No One Mentions</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Fri, 08 May 2026 22:18:23 +0000</pubDate>
      <link>https://dev.to/agentkit/hiding-underlines-on-hyperlinks-the-accessibility-bug-no-one-mentions-gi3</link>
      <guid>https://dev.to/agentkit/hiding-underlines-on-hyperlinks-the-accessibility-bug-no-one-mentions-gi3</guid>
      <description>&lt;p&gt;If you have built a website in the last decade, there is a good chance someone -- a designer, a theme developer, your own taste -- removed the underlines from your hyperlinks. The browser default is blue text with an underline, and almost every modern site has decided that the underline is ugly. The underline got swapped for a colour change, or a hover effect, or nothing at all.&lt;/p&gt;

&lt;p&gt;That choice is the single most common WCAG 2.1 failure we see in small-business audits. It is not the worst failure, and it is not the most expensive to fix. But it is everywhere, it is invisible to the people who made the decision, and on the wrong day it can be exactly the kind of thing that ends up in an ADA demand letter or an EAA enforcement notice.&lt;/p&gt;

&lt;p&gt;This article explains why a missing underline matters, what the law actually requires, and how to keep most of the visual cleanliness your designer wanted while still passing accessibility checks. No developer skills required to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WCAG actually says about link underlines
&lt;/h2&gt;

&lt;p&gt;The Web Content Accessibility Guidelines do not, in fact, require underlines on links. There is no rule that says "links must be underlined". What WCAG 2.1 does say is something more subtle, and that subtlety is where most sites trip.&lt;/p&gt;

&lt;p&gt;The relevant criterion is &lt;strong&gt;1.4.1 Use of Color&lt;/strong&gt;, a Level A requirement. It states that colour cannot be the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. In plain English: you cannot ask your users to identify what is a link purely by the fact that it is a different colour from the surrounding text.&lt;/p&gt;

&lt;p&gt;When the browser default styling is in place, links are blue and underlined. Two visual cues. Remove the underline and you are left with one cue: colour. For a user with red-green colour blindness reading a paragraph of black text with red links, the difference may be invisible. For a user reading on a low-contrast screen at an angle, the same. For a user who has overridden colours in their browser to a high-contrast mode, the link colour can be entirely lost.&lt;/p&gt;

&lt;p&gt;So WCAG does not require underlines specifically. It requires &lt;strong&gt;a non-colour way of distinguishing a link from surrounding text&lt;/strong&gt;. Underlines are by far the easiest way to provide that. Other valid options exist -- bold weight, italic, an icon, a border -- but each comes with trade-offs the typical designer has not thought through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this fails real users
&lt;/h2&gt;

&lt;p&gt;The accessibility community sometimes gets accused of writing rules for users who do not exist. The "links without underlines" issue is one where the user impact is well-documented, observable, and unambiguous.&lt;/p&gt;

&lt;p&gt;About 8% of men of Northern European descent and 0.5% of women have some form of colour vision deficiency. That is not a small group. For a deuteranopic user (the most common form, which weakens green perception), a brand-blue link in body text often appears as a slightly different shade of grey, indistinguishable from the surrounding paragraph at a glance.&lt;/p&gt;

&lt;p&gt;Users with low vision, who account for roughly 4-5% of the adult population, often run their browsers at 200% zoom or higher and rely heavily on visual contrast cues. The colour difference between a link and surrounding text shrinks at large zoom because anti-aliasing introduces more in-between pixels.&lt;/p&gt;

&lt;p&gt;Users on small mobile screens scanning a page in bright sunlight effectively have temporarily reduced colour vision. The sun washes out the screen. A link distinguished only by colour disappears.&lt;/p&gt;

&lt;p&gt;And users on overridden-colours systems -- including many users with cognitive disabilities who have configured a high-contrast mode -- may be served entirely without your colour at all. The text turns yellow on black, the link colour disappears, and the link becomes invisible.&lt;/p&gt;

&lt;p&gt;In each case, an underline solves the problem. So does any other non-colour visual indicator. Doing nothing means failing real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legal layer: EAA, ADA, and what counts as a violation
&lt;/h2&gt;

&lt;p&gt;The European Accessibility Act has been enforceable since June 2025 against businesses providing products or services to EU consumers. Section 508 has been federal law in the United States for decades. ADA Title III has been used in tens of thousands of website-accessibility lawsuits. All three reference WCAG 2.1 AA (or its national equivalent like EN 301 549 in the EU) as the conformance benchmark.&lt;/p&gt;

&lt;p&gt;When an accessibility audit finds a link without an underline or other non-colour distinction, the auditor records it as a 1.4.1 Use of Color failure. If the report ends up in the hands of a plaintiff's lawyer, that finding is used as evidence of a Level A WCAG violation, which is the most basic conformance level. Level A failures are the most commonly cited issues in accessibility lawsuits because they are unambiguous and easy to demonstrate in a courtroom: load the page, point at the link, ask "how do I know this is a link?".&lt;/p&gt;

&lt;p&gt;A single Level A failure is rarely the whole basis of a lawsuit. But it is part of the pattern that builds the case. Demand letters often list 8-15 specific WCAG failures, and a missing-underline issue is almost always one of them because it is so easy to find and so common across page templates. A site that fixes it removes one of the easiest-to-prove pieces of evidence against itself. Our &lt;a href="https://dev.to/blog/overlay-widget-ada-demand-letter/"&gt;overlay widget demand letter explainer&lt;/a&gt; walks through what these letters actually look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five fixes (in order of how much you keep your design)
&lt;/h2&gt;

&lt;p&gt;Designers remove underlines because the default underline looks dated and dense, especially in long-form content with many links. Reasonable enough. The good news is that there are several ways to satisfy WCAG that preserve almost everything your designer cared about. Pick the one that fits your aesthetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 1: Restore underlines but style them.&lt;/strong&gt; The browser default underline runs at 1px, sits flush against the baseline of the text, and matches the link colour. Modern CSS lets you change all three. Set the underline thickness to 2px so it is visible without being heavy. Add a 2-3px offset so the underline sits below the baseline rather than crashing into the descenders of letters like g, j, p, and q. Set the underline colour to a slightly muted version of your link colour. The result is an underline that looks intentional and modern rather than dated.&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;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;text-underline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 2: Underline only on hover and focus, but only for navigation links.&lt;/strong&gt; This is the tightest acceptable compromise. It only works for links that are visually segregated from body text -- navigation menus, footer columns, tile-style link cards. In those locations, the surrounding visual context (a horizontal nav bar, a list-of-links section, a card with a button-shaped affordance) tells the user "this whole region is interactive". Add an underline on hover and on focus so the keyboard user gets feedback. Do &lt;strong&gt;not&lt;/strong&gt; apply this rule to links inside paragraph text, where there is no surrounding context to communicate "this is a link".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 3: Use bold weight for inline links.&lt;/strong&gt; If your typography system has clear contrast between regular and bold weights (most modern typefaces do), bolding inline links provides a non-colour distinction. Combine the bold weight with your brand link colour and you have two cues, satisfying 1.4.1 cleanly. This works best with serif typefaces where the weight difference is very visible. Sans-serif typefaces with subtle weight gradations may not provide enough difference for low-vision users to detect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 4: Add a small icon after external links.&lt;/strong&gt; A common pattern in newsletters and publications is to add a tiny external-link icon (an arrow leaving a square) after links that point off-site. The icon provides a non-colour cue and also tells the user where the link goes. This works well as a complementary cue but is not enough on its own for inline body links unless every link gets some icon, which gets visually noisy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 5: Just keep the underline.&lt;/strong&gt; Long-form publications -- The Guardian, NYT, The Atlantic -- mostly kept underlines on inline links because their editors care about readability over visual minimalism. The underline on body links is the most usable pattern and the one that requires the least cleverness. Many design systems are quietly returning to it after a decade of underline-less designs.&lt;/p&gt;

&lt;p&gt;Whichever fix you choose, the criterion is the same: a user looking at a page must be able to identify links without relying on colour alone. Walk through your site's main page templates and check each link type -- body text links, navigation links, footer links, breadcrumb links, button-styled links, and "read more" links -- against that criterion. Most sites will need to fix two or three of those link types, not all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own site in three minutes
&lt;/h2&gt;

&lt;p&gt;Open your site in a browser. Pick any page that contains a paragraph with at least one inline link in it -- a blog post, an FAQ entry, an "about us" page. Then run this quick check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The grayscale test.&lt;/strong&gt; Apply a grayscale filter to your screen using your operating system's accessibility settings (on macOS, System Settings &amp;gt; Accessibility &amp;gt; Display &amp;gt; Color Filters &amp;gt; Grayscale; on Windows 11, Settings &amp;gt; Accessibility &amp;gt; Color Filters &amp;gt; Grayscale). Look at the paragraph. Can you identify the link without the colour difference? If no, the link fails 1.4.1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The squint test.&lt;/strong&gt; Without changing any settings, lean back from your screen and squint your eyes. The visual cues that survive squinting are the ones that work for users with low vision and for users on bright outdoor screens. If the link disappears when you squint, it is too dependent on colour.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The high-contrast test.&lt;/strong&gt; Switch your operating system into high-contrast mode (System Settings &amp;gt; Accessibility &amp;gt; Display &amp;gt; Increase Contrast on macOS; Windows + U then High Contrast on Windows). The brand colours often disappear in this mode. Can you still identify the link?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your site fails any of these tests, the fix is one CSS rule change away. If you do not have a developer on hand, our &lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;five-minute accessibility audit guide&lt;/a&gt; and the &lt;a href="https://dev.to/blog/test-website-voiceover-no-coding/"&gt;test-your-site-with-VoiceOver guide&lt;/a&gt; walk through the broader checks you can run yourself in under an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you keep finding it everywhere
&lt;/h2&gt;

&lt;p&gt;The reason this issue is so widespread is that page-builder defaults and theme defaults overwhelmingly favour underline-less links. Squarespace, Wix, Webflow, Framer, and most premium WordPress themes ship with &lt;code&gt;text-decoration: none&lt;/code&gt; on links by default. Designers building inside those tools rarely override the default because the default looks like the design template they are mimicking.&lt;/p&gt;

&lt;p&gt;The practical consequence is that the underline-less link pattern propagated across the web through the platform layer rather than through any individual decision. You did not choose this for your site. Your platform did. And your platform did not warn you that the default fails WCAG 2.1 AA Level A.&lt;/p&gt;

&lt;p&gt;The fix sits in a single CSS rule for most sites. The hardest part is convincing the designer who removed the underline that adding one back -- styled tastefully -- is worth a slightly less minimalist look. The argument for them is that the alternative is failing a Level A WCAG criterion, which carries real legal exposure and excludes a meaningful share of your potential customers from finding their way through your site.&lt;/p&gt;

&lt;p&gt;Pick a Tuesday afternoon. Apply the grayscale filter. Walk three pages. Decide which fix fits your design. Ship it. The bug is small, the fix is small, and the pattern is so common that fixing it puts you ahead of most of your competitors before lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;Five-Minute Accessibility Audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/lighthouse-score-95-still-get-sued/"&gt;Why Lighthouse Score 95 Doesn't Stop You Getting Sued&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/color-contrast-guide/"&gt;Color Contrast Guide for Non-Developers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Test Your Website with VoiceOver in 10 Minutes (No Coding Required)</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Fri, 08 May 2026 22:17:40 +0000</pubDate>
      <link>https://dev.to/agentkit/how-to-test-your-website-with-voiceover-in-10-minutes-no-coding-required-399l</link>
      <guid>https://dev.to/agentkit/how-to-test-your-website-with-voiceover-in-10-minutes-no-coding-required-399l</guid>
      <description>&lt;p&gt;Automated scanners catch about 30 to 40 percent of accessibility problems. The rest -- the ones that actually frustrate real users and trigger ADA demand letters -- only show up when a screen reader meets your site. The good news: you do not need to be a developer to listen.&lt;/p&gt;

&lt;p&gt;Every Mac, iPhone, and iPad ships with a screen reader called VoiceOver. It is free, it is already installed, and you can be using it in about thirty seconds. This guide walks you through ten minutes of structured listening, gives you a checklist of what to listen for, and tells you exactly what to write down for your developer or designer to fix. No DevTools. No jargon. No code.&lt;/p&gt;

&lt;p&gt;If you do not own a Mac, the same approach works with NVDA on Windows (free download from nvaccess.org) or TalkBack on Android. The keystrokes differ, but the listening framework is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why listen, when scanners are so much faster
&lt;/h2&gt;

&lt;p&gt;Lighthouse, axe, WAVE, and Pa11y are valuable. Run them. They will catch missing alt text, low color contrast, missing form labels, and hundreds of other defects that are easy for a machine to detect. But there is an entire category of accessibility failure that no automated tool can catch: the user-experience failure.&lt;/p&gt;

&lt;p&gt;A button can have a perfect aria-label and still be impossible to find. A form can have every field labeled correctly and still be unusable because the error messages disappear before they are read. A photo gallery can pass every automated test and still trap a screen-reader user inside a modal they cannot escape. These are the issues that show up in lawsuits. The plaintiff's attorney does not run Lighthouse. The plaintiff's attorney sits down with a screen reader and tries to complete a real task -- buy a product, schedule an appointment, fill out a form -- and writes down everywhere it breaks.&lt;/p&gt;

&lt;p&gt;Ten minutes of structured listening will tell you more about your real risk than any automated report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start: get comfortable with the on-off switch
&lt;/h2&gt;

&lt;p&gt;The single thing that scares non-developers away from screen-reader testing is the fear that you will turn it on and not be able to turn it off. So before you do anything else, learn the switch.&lt;/p&gt;

&lt;p&gt;On a Mac: hold Command and press F5. That is the toggle. Press it once to turn VoiceOver on. Press it again to turn it off. If your Mac has a Touch ID button, the same shortcut still works -- you can also triple-press the Touch ID button. On a brand-new MacBook with no F5 key visible, you may need to hold the Function (fn) key plus F5.&lt;/p&gt;

&lt;p&gt;On an iPhone or iPad: triple-click the side button (or Home button on older models). It is worth setting this up in Settings -&amp;gt; Accessibility -&amp;gt; Accessibility Shortcut -&amp;gt; VoiceOver before you start, so the triple-click does the right thing.&lt;/p&gt;

&lt;p&gt;On Windows with NVDA: press Insert + Q to quit, or Caps Lock + Q if you set NVDA to use Caps Lock as the modifier.&lt;/p&gt;

&lt;p&gt;Test the toggle a few times before you start your real testing. When you can confidently turn the screen reader on and off, you are ready. Take the keyboard. Set the mouse aside.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten-minute test plan
&lt;/h2&gt;

&lt;p&gt;Set a timer for ten minutes. Open your website in a fresh browser tab. Turn VoiceOver on (Command + F5). Now pretend you are a customer trying to do the most important thing on your site: buy something, book an appointment, request a quote, sign up for a newsletter, contact you.&lt;/p&gt;

&lt;p&gt;You are going to perform this task using only the keyboard. The Tab key moves you forward. Shift + Tab moves you backward. The arrow keys, on a Mac with VoiceOver running, move through everything on the page when used with the VO modifier (Control + Option). For most sites, plain Tab is enough to start.&lt;/p&gt;

&lt;p&gt;As you go, write down -- on paper, in a Notes file, anywhere -- every place where one of the following happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1: Can you tell what page you are on?
&lt;/h3&gt;

&lt;p&gt;When VoiceOver starts reading your page, the first thing it should announce is the page title and the main heading. If it just starts reading "menu, link, link, link, link," your site has a heading-structure problem. Real screen-reader users skip directly to a page's main heading using a shortcut (VO + Command + H on Mac). If your page has no clear h1, or has multiple h1s, or has its main heading buried below decorative content, real users get lost.&lt;/p&gt;

&lt;p&gt;Write it down: "Heading structure is broken on the homepage."&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 2: Can you skip past the menu?
&lt;/h3&gt;

&lt;p&gt;Most websites have a long navigation menu at the top of every page. A sighted user's eye skips over it instantly. A screen-reader user has to listen to it on every page -- unless your site has a "skip to main content" link as the very first focusable element.&lt;/p&gt;

&lt;p&gt;Press Tab once on a fresh page load. Listen to what VoiceOver announces. If the first thing it says is "Skip to main content, link," you have one. If it announces a logo or the first navigation item, you do not. Add it to your list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 3: Are images described, or are they silent?
&lt;/h3&gt;

&lt;p&gt;Move through your homepage with Tab and the arrow keys. Listen for VoiceOver to announce images. Decorative images should be silent or announce themselves as "decorative." Meaningful images -- product photos, team headshots, infographics -- should announce a meaningful description ("White ceramic mug with the company logo," not "IMG underscore 4382 dot JPG" or, worse, dead silence).&lt;/p&gt;

&lt;p&gt;For every product photo, before-and-after gallery, or hero image that announces a filename or stays silent, write down: "Alt text missing or wrong on [page name]."&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 4: Can you fill out the form?
&lt;/h3&gt;

&lt;p&gt;Find your most important form -- contact, booking, checkout, signup -- and try to fill it out using only the keyboard. Tab into the first field. Listen for VoiceOver to announce the field label and any required-field indicator. Type something. Tab to the next field.&lt;/p&gt;

&lt;p&gt;The two failures you will hear most often are: a field that announces nothing but "edit text" (no label), and a field that announces a placeholder ("name," "email") that disappears once you start typing, leaving the screen-reader user with no way to know what they are filling in.&lt;/p&gt;

&lt;p&gt;Submit the form intentionally wrong -- leave a required field empty. Listen for an error message. Did VoiceOver announce it? Or did a red border just appear silently next to the empty field? If the error did not announce, write it down: "Form errors are silent."&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 5: Can you escape the modal?
&lt;/h3&gt;

&lt;p&gt;If your site has a popup -- newsletter modal, cookie banner, age-gate, photo lightbox -- trigger it. Try to close it using only the keyboard. The Escape key should close it. Tab should move only between the elements inside the modal (it should "trap" focus, in accessibility jargon). When the modal closes, focus should return to the element that opened it.&lt;/p&gt;

&lt;p&gt;If you press Escape and nothing happens, write it down. If you press Tab and end up reading the page behind the modal, write it down. If the modal closes but you have no idea where you are on the page, write it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 6: Can you complete one full task?
&lt;/h3&gt;

&lt;p&gt;This is the most important test. Pretend you are a real customer. Try to complete one full transaction or task -- add an item to the cart and check out, book an appointment, submit a contact form, schedule a call, sign up for a newsletter. Use only the keyboard. Listen, do not look.&lt;/p&gt;

&lt;p&gt;Did you finish it? How long did it take? Where did you get stuck? What did you have to guess at? What had to be repeated? If you, the business owner, with no disability and full knowledge of how your own site is supposed to work, struggled to complete the task, your real users with disabilities are not completing it at all. They are leaving and never coming back.&lt;/p&gt;

&lt;p&gt;That is your most important finding. Write it at the top of your list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning your list into a fix plan
&lt;/h2&gt;

&lt;p&gt;When the timer goes off, turn VoiceOver off (Command + F5 again). Look at your notes. Most ten-minute tests produce between five and fifteen items. They will fall into roughly three buckets.&lt;/p&gt;

&lt;p&gt;The first bucket is content fixes. Missing alt text. Wrong heading levels. Forms with missing labels. Buttons that say "click here" instead of describing the action. These are usually fixable by anyone who can edit content in your CMS, in under an hour. You may not need a developer.&lt;/p&gt;

&lt;p&gt;The second bucket is template fixes. A missing skip-link. A modal that does not trap focus. An error message that does not announce. These usually require editing a theme or a component. They are a developer task, but a small one -- typically a few hours.&lt;/p&gt;

&lt;p&gt;The third bucket is third-party tool fixes. A booking widget that locks out the keyboard. A live-chat bubble with no accessible name. A newsletter signup popup that hijacks focus. These require either replacing the tool, asking the vendor for a fix, or hiding the tool from screen-reader users while you find a better option. They are the hardest to fix on your own and often the highest legal risk.&lt;/p&gt;

&lt;p&gt;Send the full list to your developer or your accessibility consultant. If you do not have either, our &lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;free five-minute accessibility audit&lt;/a&gt; covers the automated side, and we are working on a tool that pulls everything together for non-developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you have just done is the test plaintiffs run
&lt;/h2&gt;

&lt;p&gt;Plaintiff-side accessibility lawsuits start with one of two things: an automated scan flagging dozens of WCAG failures, or a human tester sitting down with a screen reader and trying to complete a task. The lawsuits that actually go somewhere -- the ones with detailed factual allegations and serious settlement leverage -- always include the second kind of evidence. "Plaintiff attempted to schedule an appointment on the booking page using the NVDA screen reader. After approximately fifteen minutes of attempts, plaintiff was unable to select a time slot." That sentence, in some form, appears in dozens of demand letters every month.&lt;/p&gt;

&lt;p&gt;What you just did, in ten minutes, is the same exercise. You now know roughly what a tester would find. You can fix it before they show up. That is the entire point of doing this yourself.&lt;/p&gt;

&lt;p&gt;You do not need to do this every week. Doing it once a quarter -- and any time you launch a new page, change a major template, or add a new third-party tool -- is enough to catch the issues that matter. Many small business owners find that the first ten-minute test produces the longest list, and subsequent tests get shorter and faster.&lt;/p&gt;

&lt;p&gt;Run it once. Get the list. Fix the easy ones today. Send the harder ones to your developer this week. Then sleep better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;The Five-Minute Accessibility Audit Anyone Can Run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/screen-reader-friendly-website-guide/"&gt;Screen Reader Friendly Website Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/lighthouse-score-95-still-get-sued/"&gt;Why a Lighthouse Score of 95 Won't Save You From a Lawsuit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Image Carousels and ADA Lawsuits: Why Auto-Rotating Sliders Keep Getting Small Businesses Sued</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Fri, 08 May 2026 22:16:47 +0000</pubDate>
      <link>https://dev.to/agentkit/image-carousels-and-ada-lawsuits-why-auto-rotating-sliders-keep-getting-small-businesses-sued-34hh</link>
      <guid>https://dev.to/agentkit/image-carousels-and-ada-lawsuits-why-auto-rotating-sliders-keep-getting-small-businesses-sued-34hh</guid>
      <description>&lt;p&gt;If you own a small business website built between 2014 and 2022, there is a very good chance your homepage starts with a giant rotating image carousel. Three to five photos, each with a headline and a button, fading or sliding from one to the next every four seconds. Your designer probably loved it. Your conversion rate consultant probably hated it. And in 2026, the plaintiff's lawyer who just sent you a demand letter probably mentioned it by name.&lt;/p&gt;

&lt;p&gt;Image carousels are one of the most-cited widgets in ADA Title III filings against small businesses, second only to inaccessible PDFs. They are also one of the most common sources of European Accessibility Act complaints reaching national market surveillance authorities. The reason is not that carousels are impossible to build accessibly — they are, with effort — but that almost nobody actually does. The default carousel that ships with your theme is, in plain language, broken for several categories of disabled users.&lt;/p&gt;

&lt;p&gt;This guide explains exactly why, what a plaintiff's lawyer typically argues, and what your three real options are if you find one on your site. It is written for non-developers. You do not need to know any code to make a decision today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a carousel does to a screen reader user
&lt;/h2&gt;

&lt;p&gt;When a carousel auto-rotates, a screen reader user has no way to know that the visible content has changed. Most stock carousels do not announce slide changes through an ARIA live region. So a user who has just landed on your homepage hears the alt text of slide one, starts reading the headline, and four seconds later the visible content has switched to slide three — but the screen reader is still reading slide one. The user reaches the call-to-action button, presses Enter, and lands on a page that has nothing to do with what they thought they were clicking.&lt;/p&gt;

&lt;p&gt;If the carousel does have a live region, it is often misconfigured to announce the entire slide every time, so the user hears headlines from three different slides while trying to read a paragraph elsewhere on the page. The carousel becomes the loudest thing in the room, and the user cannot escape it without leaving the site.&lt;/p&gt;

&lt;p&gt;Either failure mode is a violation of WCAG Success Criterion 4.1.3 (Status Messages) and, depending on configuration, 1.3.1 (Info and Relationships) and 2.2.2 (Pause, Stop, Hide).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a carousel does to a keyboard-only user
&lt;/h2&gt;

&lt;p&gt;A keyboard-only user — including most motor-impaired users, many older users with tremor or arthritis, and most screen reader users — navigates a page with the Tab key. A well-built carousel exposes its previous, next, and pause controls in a logical tab order with visible focus rings, so a keyboard user can move into the carousel, stop it, and step through slides at their own pace.&lt;/p&gt;

&lt;p&gt;Almost no stock carousel does this. The default behaviour for most theme carousels in 2026 is one of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The previous and next arrows are decorative div elements with click handlers but no keyboard handlers, so the keyboard user cannot reach them at all.&lt;/li&gt;
&lt;li&gt;The slide indicators (the dots beneath the carousel) are reachable by Tab but have no visible focus indicator, so the user cannot tell where they are.&lt;/li&gt;
&lt;li&gt;There is no pause button, full stop. WCAG 2.2.2 requires that any moving content lasting longer than five seconds have a mechanism to pause, stop, or hide it. A carousel that auto-advances every four seconds and never stops fails this criterion outright.&lt;/li&gt;
&lt;li&gt;Tabbing into the carousel moves focus into a hidden slide that is not visible on the screen, so the user's focus disappears and they cannot tell where they are.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a separate WCAG failure under 2.1.1 (Keyboard), 2.4.7 (Focus Visible), or 2.2.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a carousel does to a user with a vestibular disorder
&lt;/h2&gt;

&lt;p&gt;For users with vestibular disorders, motion sensitivity, or migraine triggers, an auto-rotating carousel is not just an annoyance — it is a physical symptom trigger. The horizontal movement, particularly fast slide transitions or parallax effects, can induce nausea, dizziness, and full migraine within seconds.&lt;/p&gt;

&lt;p&gt;WCAG 2.3.3 (Animation from Interactions) and the related &lt;code&gt;prefers-reduced-motion&lt;/code&gt; media query require sites to respect a user's operating-system preference for reduced motion. A carousel that auto-advances regardless of that preference is a Level AAA failure today and, given the direction of WCAG 3.0 drafts, very likely a Level AA failure within the next standard cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a carousel does to a user with low vision
&lt;/h2&gt;

&lt;p&gt;Users with low vision who zoom their browser to 200 or 400 percent need content to reflow without horizontal scrolling, per WCAG 1.4.10 (Reflow). Most carousels are built with fixed pixel widths or absolute positioning that does not reflow. At 320 pixels of effective viewport width — the standard reflow target — the carousel often stops working entirely, with text running off the side of the screen and buttons becoming unreachable.&lt;/p&gt;

&lt;p&gt;Color contrast is the other problem. Most carousels overlay headline text on a photo. The photo's brightness varies across the image, so a single text colour cannot meet the 4.5:1 contrast ratio required by WCAG 1.4.3 across the entire span of the headline. The accessible solution is a solid or near-solid overlay between the photo and the text, but that often gets removed during a redesign because it does not look as polished.&lt;/p&gt;

&lt;h2&gt;
  
  
  What plaintiffs actually claim
&lt;/h2&gt;

&lt;p&gt;Demand letters and complaints filed in 2024-2026 against small businesses with carousels typically allege some combination of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The carousel auto-advances and provides no pause, stop, or hide control, citing WCAG 2.2.2.&lt;/li&gt;
&lt;li&gt;The slide change is not announced to screen reader users, citing WCAG 4.1.3.&lt;/li&gt;
&lt;li&gt;The previous, next, and indicator controls are not reachable by keyboard or have no visible focus indicator, citing WCAG 2.1.1 and 2.4.7.&lt;/li&gt;
&lt;li&gt;The headline text fails colour contrast against the underlying photograph, citing WCAG 1.4.3.&lt;/li&gt;
&lt;li&gt;The carousel does not respect the user's reduced-motion preference, citing WCAG 2.3.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In most cases, the plaintiff's expert simply opens the homepage with a screen reader and a keyboard, and screenshots or records the failures. The carousel is often the easiest thing to demonstrate, which is why it shows up so frequently as the lead exhibit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your three options
&lt;/h2&gt;

&lt;p&gt;If you have just discovered a carousel on your homepage and your lawyer is asking what to do, you have three realistic choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Remove the carousel entirely
&lt;/h3&gt;

&lt;p&gt;This is the cheapest, fastest, and most defensible option, and it is what most accessibility consultants quietly recommend. Replace the carousel with a single hero image and a single call-to-action. The conversion data is on your side: most studies of carousel performance show that only the first slide gets meaningful clicks, with each subsequent slide capturing under two percent of attention. You are not giving up much.&lt;/p&gt;

&lt;p&gt;This option resolves every WCAG failure listed above in a single change. From a liability standpoint, it eliminates the most-cited element in your demand letter. From a content standpoint, it forces you to decide what your homepage's single most important message is, which is usually a healthy exercise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Stop the auto-rotation
&lt;/h3&gt;

&lt;p&gt;If your designer or your boss insists on multiple slides, stop the carousel from auto-advancing. The user can then click previous and next to move through slides at their own pace, and the most severe failures (2.2.2 and 2.3.3) go away. You still need to fix keyboard focus, screen reader announcements, contrast, and reflow, but you have removed the failures that are typically cited first.&lt;/p&gt;

&lt;p&gt;This is a middle-ground option. It is cheaper than rebuilding the carousel from scratch but more expensive than removing it, and it leaves several WCAG criteria still requiring work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Rebuild the carousel correctly
&lt;/h3&gt;

&lt;p&gt;A correctly built carousel is possible. It uses an accessible pattern such as the ARIA Authoring Practices Guide carousel pattern, with proper pause and play buttons, keyboard-reachable slide controls, an ARIA live region for slide change announcements, visible focus indicators, contrast-checked text overlays, reflow at 320 pixels, and a &lt;code&gt;prefers-reduced-motion&lt;/code&gt; check that disables the rotation for users who request it.&lt;/p&gt;

&lt;p&gt;This is real engineering work. Expect to spend anywhere from several hundred to several thousand dollars depending on your platform and the complexity of your carousel. It is the right choice if the carousel is genuinely critical to your conversion strategy and you have the budget. For most small businesses, options one or two are more proportionate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do this week
&lt;/h2&gt;

&lt;p&gt;If you have a demand letter in hand, talk to your lawyer first. The list above is not legal advice.&lt;/p&gt;

&lt;p&gt;If you do not have a demand letter and you are reading this proactively — which is the right time to act — open your homepage on a desktop browser and try three things. Press Tab from the URL bar and count how many tab stops it takes to escape the carousel. Open your operating system's accessibility settings, turn on the reduced-motion preference, and reload the page. Then turn on your built-in screen reader (VoiceOver on Mac, Narrator on Windows) and listen to what happens when the slides change.&lt;/p&gt;

&lt;p&gt;If any of those tests fails or surprises you, you have a finding. Document what you saw, decide which of the three options fits your budget and your business, and act on it. Carousel issues do not get better on their own, and the cost of a single ADA demand letter — typically several thousand dollars in legal fees plus the cost of remediation — is almost always higher than the cost of just removing the widget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/why-accessibility-overlays-dont-work/"&gt;Why your accessibility overlay won't save you from an ADA lawsuit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/accessible-modals-popups-guide/"&gt;Accessible modals and popups: a non-developer's guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/third-party-widget-accessibility-guide/"&gt;Third-party widgets and accessibility: who is liable when an embed breaks WCAG&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Your Online Booking Widget Might Be Your Biggest ADA Risk</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Tue, 05 May 2026 05:00:36 +0000</pubDate>
      <link>https://dev.to/agentkit/why-your-online-booking-widget-might-be-your-biggest-ada-risk-5cd8</link>
      <guid>https://dev.to/agentkit/why-your-online-booking-widget-might-be-your-biggest-ada-risk-5cd8</guid>
      <description>&lt;p&gt;If you run a service business — a tutoring center, an auto repair shop, a therapist's office, a wedding photographer, a salon, a dental practice, a tax preparer — there is a good chance the page on your website that gets the most use is the one with your booking widget on it. That booking widget is also, statistically, the most likely thing on your entire site to get you sued.&lt;/p&gt;

&lt;p&gt;We say that with data. Across the demand letters and complaints we've reviewed in the last twelve months, the appointment-scheduling widget is the single most cited failure point. It comes up more often than the homepage. More often than checkout. More often than the contact form. And it comes up in cases against businesses where every other part of the site looks reasonable.&lt;/p&gt;

&lt;p&gt;This article explains, in plain English, why booking widgets fail accessibility audits so reliably, what a passing one actually looks like, and what you can do about it without becoming a developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Booking Widgets Fail So Often
&lt;/h2&gt;

&lt;p&gt;The popular booking platforms — Calendly, Acuity Scheduling, SimplyBook.me, Setmore, vCita, Square Appointments, Bookeo, BookSteam, MindBody, JaneApp, Cliniko, Chiro8000, ClassPass for studios — were not built with accessibility as a foundational requirement. Most of them were built around 2014–2018 by small teams optimizing for one thing: the conversion rate of a person with a mouse on a desktop computer.&lt;/p&gt;

&lt;p&gt;That trade-off shows up in three places.&lt;/p&gt;

&lt;p&gt;First, the date picker. The little calendar grid where you choose the day of your appointment is built almost universally as a mouse-driven component. You hover over a date, click it, and the time slots appear. If you can't use a mouse — because you have a tremor, because you have low vision, because you use a screen reader, because you're on a phone — the calendar grid is often a wall. Tab might land you somewhere random. Arrow keys might not move between dates. The "next month" button might not have a label that any assistive technology can understand.&lt;/p&gt;

&lt;p&gt;Second, the time slots. After you pick a date, you see a row or grid of available times. Critical detail: in most widgets, time-slot status is conveyed entirely through color. Available slots are blue. Unavailable slots are gray. There is no text label, no icon, nothing programmatic. A blind user listening to the page through a screen reader hears "9:00 AM, button. 9:30 AM, button. 10:00 AM, button." All the slots sound identical. They have no way to know which ones are actually available without trying every single one and getting an error.&lt;/p&gt;

&lt;p&gt;Third, the form itself. Once you've picked a time, you fill in your name, email, phone, and any service-specific details. Booking widgets are notorious for using &lt;strong&gt;placeholder text instead of labels&lt;/strong&gt;. The greyed-out "Your name" text inside the input box looks like a label, but it disappears the moment you start typing, and screen readers don't always announce it. So a screen reader user who tabs into the form hears nothing — they have no idea what they're supposed to type into that empty box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Color Alone" Means and Why It's a Lawsuit Magnet
&lt;/h2&gt;

&lt;p&gt;WCAG 2.1 Success Criterion 1.4.1, "Use of Color," is short. The text is two sentences: color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element.&lt;/p&gt;

&lt;p&gt;The reason this single criterion shows up so often in booking-widget complaints is that color-only status is genuinely unfixable from outside the widget. You can't write CSS that adds a screen-reader-readable label to a third-party iframe. Either the vendor exposes proper text and ARIA, or they don't. And many of them don't.&lt;/p&gt;

&lt;p&gt;A widget that conveys availability with color alone fails 1.4.1. It also typically fails 4.1.2 ("Name, Role, Value") because the buttons don't expose their availability state programmatically. It often fails 2.1.1 ("Keyboard") because you can't get to the buttons with a keyboard at all. So a single design decision — using color to mean "available" — frequently chains into three or four separate WCAG failures, each of which is independently litigable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Tell, Without a Developer, Whether Your Widget Passes
&lt;/h2&gt;

&lt;p&gt;You don't need DevTools and you don't need to read the WCAG guidelines yourself. Here are five things you can check in about ten minutes on your own booking page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Unplug your mouse and try to book an appointment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use only the Tab key, the arrow keys, and Enter. Try to navigate from your name field, through the calendar, into the time-slot grid, and complete the booking. If you get stuck — if Tab skips past the calendar entirely, or if you can't move between dates, or if you can't tell which time slot is currently focused — that is a failure of WCAG 2.1.1 (Keyboard) and 2.4.7 (Focus Visible). It is also the single most common failure pattern in the complaints we've seen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Turn on your phone's screen reader and try to book.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On iPhone: Settings → Accessibility → VoiceOver. On Android: Settings → Accessibility → TalkBack. Both will start reading the screen aloud as you swipe. Open your booking page. Swipe through the calendar. Listen to what the dates and time slots sound like. If the time slots all sound identical regardless of whether they're available, that's a 1.4.1 failure. If form fields read as "edit text, blank" with no label, that's a 3.3.2 failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Zoom your browser to 200% and try to book.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In any desktop browser, hold Cmd (Mac) or Ctrl (Windows) and press the plus key several times until the page is at 200%. WCAG 1.4.4 (Resize Text) requires that all functionality remain available. Watch what happens to the calendar. Many widgets become unusable at 200% — the date grid scrolls horizontally and you can't see the buttons, or pieces overlap, or the "next" button moves off-screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Look at the slot colors and ask: would I know which ones are available if I were color blind?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take a screenshot of your time-slot grid and run it through a free color-blind simulator (search "color blindness simulator" — they're free, browser-based, and don't require an account). If, in any of the simulated views, you can no longer tell available slots from unavailable ones, your widget fails 1.4.1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Look for an "Accessibility" or "VPAT" link in the vendor's footer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every reputable booking platform should publish a Voluntary Product Accessibility Template — a self-assessment of how their product conforms to WCAG. If your vendor doesn't publish one, that itself is a yellow flag. If they do publish one, read the "Does Not Support" rows. Many widget VPATs explicitly admit that the calendar grid does not support keyboard navigation or that time-slot status is conveyed by color. That admission is what plaintiffs' firms cite in demand letters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do If Yours Fails
&lt;/h2&gt;

&lt;p&gt;You have three realistic paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path one: switch vendors.&lt;/strong&gt; Among the major booking platforms, Cal.com is currently the most accessibility-forward, with a published commitment to WCAG 2.1 AA and an ongoing remediation roadmap. Acuity Scheduling has improved substantially since the 2024 Squarespace acquisition rolled accessibility resources into the team, but the calendar grid still has documented keyboard-navigation gaps. SimplyBook.me, Setmore, and BookSteam have been the most frequently cited vendors in 2025 demand letters. None of this is legal advice — vendors can change quickly — but as of this writing, switching from a frequently-cited vendor to Cal.com is a defensible remediation plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path two: build a custom flow.&lt;/strong&gt; Most modern frameworks have well-documented accessible date-picker patterns. A developer can build a server-rendered form with a date input that uses native browser controls (which are accessible by default on every modern OS) and a list of time slots rendered as actual buttons with text labels. This is a one-to-three-day job for a competent web developer and removes the third-party-widget dependency entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path three: provide a documented accessible alternative.&lt;/strong&gt; ADA Title III allows for "auxiliary aids and services" as long as they provide equivalent access. For a booking widget, that means: a clearly published phone number AND an email contact form, both staffed during the same hours as your online widget, with the same scope of services available. The published-phone-number approach alone is not sufficient — courts have rejected "call us" as an alternative to an inaccessible web flow when the website otherwise allows the user to do everything else online. But phone plus email plus a documented commitment to remediate the widget itself is a defensible interim posture while you do path one or path two.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in a Demand Letter
&lt;/h2&gt;

&lt;p&gt;We've reviewed enough demand letters to recognize the pattern. The plaintiff's firm scrapes service-business websites with an automated checker (axe-core, ARC Toolkit, or a custom crawler). When the crawler hits a third-party booking widget, it logs the failures: missing labels, color-only state, no keyboard support, focus traps, missing landmarks. The firm pulls a handful of the failures into a complaint, attaches screenshots, and sends a demand for $5,000–$25,000 plus a remediation commitment.&lt;/p&gt;

&lt;p&gt;The single defense that consistently reduces settlement amounts is showing that the failures are with a third-party vendor that you have already begun migrating away from, with a documented commitment to a deadline. Every other defense is weaker. "We didn't know" is not a defense. "Our budget is small" is not a defense. "Our customers haven't complained" is the one judges most often dismiss out of hand, because the complaint is being brought specifically by a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quiet Part: Most Widgets Will Get Better, Slowly
&lt;/h2&gt;

&lt;p&gt;The honest forecast: vendors are responding. Cal.com has been ahead of the field for two years. Acuity is investing in remediation. Calendly published a commitment in late 2025 to reach WCAG 2.1 AA across the entire product by Q3 2026. The European Accessibility Act, which took effect in June 2025, is forcing vendors with EU customers to ship accessibility fixes faster than they otherwise would have.&lt;/p&gt;

&lt;p&gt;But "faster" is still slow. If you're operating today, in 2026, with a frequently-cited vendor and an unaccessible flow, waiting for the vendor is not a strategy. The cost of waiting is a $5,000–$25,000 demand letter, and those continue to land daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;The Five-Minute Accessibility Audit Anyone Can Run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/accessible-booking-systems-guide/"&gt;Accessible Booking Systems Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/ada-lawsuits-small-business/"&gt;ADA Lawsuits and Small Business: What Actually Happens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Six Accessibility Failures We Keep Finding in WooCommerce Stores (And the Plugins That Cause Them)</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Mon, 04 May 2026 05:00:59 +0000</pubDate>
      <link>https://dev.to/agentkit/six-accessibility-failures-we-keep-finding-in-woocommerce-stores-and-the-plugins-that-cause-them-1lpf</link>
      <guid>https://dev.to/agentkit/six-accessibility-failures-we-keep-finding-in-woocommerce-stores-and-the-plugins-that-cause-them-1lpf</guid>
      <description>&lt;p&gt;WooCommerce sits underneath a large share of small-business e-commerce on the web. It is free, runs on top of WordPress, and is flexible enough that a hobbyist can launch a store in an afternoon. That same flexibility is also why WooCommerce stores show up in ADA demand letters and EAA complaints out of proportion to their share of the market. The owner picked a theme that looked nice, installed five plugins to add the features they needed, and the combined output failed a half dozen WCAG criteria nobody on the team has ever heard of.&lt;/p&gt;

&lt;p&gt;We have audited a long string of WooCommerce stores in the last six months — small bakeries, indie clothing brands, a couple of supplement shops, a regional book chain, a handmade-jewelry seller, a coffee roaster. Every one of them had the same six problems. None of the owners knew. Every one of the owners is exposed to the same demand-letter campaigns that have been hitting Shopify and BigCommerce stores for the last two years. This post is the list, in order of how often we see it, with what each one costs and how a non-developer can fix it without rebuilding the store.&lt;/p&gt;

&lt;p&gt;None of this is legal advice. If you have already received a demand letter, talk to an attorney with ADA Title III or EAA experience before responding.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Product images with alt text that says nothing
&lt;/h2&gt;

&lt;p&gt;The number-one finding across every WooCommerce audit is product images whose alt text is the file name or the product name copy-pasted twenty times in a row. Lighthouse passes the page because the alt attribute is present. A screen reader user shopping the store hears "DSC_4271, DSC_4271, DSC_4271" or "Blue Cotton T-Shirt, Blue Cotton T-Shirt, Blue Cotton T-Shirt" depending on which is worse. They cannot tell the listings apart and they leave. This is the failure pattern most commonly cited in recent demand letters against e-commerce stores.&lt;/p&gt;

&lt;p&gt;WooCommerce makes this worse than necessary because the default image upload flow does not require alt text and most themes use the product name as the fallback. The fallback is technically present and technically wrong, in the sense that a screen reader user gets the same string for every product variant or color swatch on the page.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: open every product, click each image in the WordPress media library, and write alt text that describes what is visually different about that specific image. For a product page with five images of the same shirt from five angles, alt text might be "front view of blue cotton t-shirt", "back view showing pocket detail", "close-up of stitched neckline", "model wearing shirt with jeans for scale", "flat-lay of folded shirt". For a category page showing many products, the alt text on each thumbnail should distinguish the product, not repeat the title that already appears as text next to it. We wrote a longer guide on &lt;a href="https://dev.to/blog/alt-text-guide/"&gt;alt text patterns that pass and fail&lt;/a&gt; if you want examples that go past the basics.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The "Add to Cart" button is a div, not a button
&lt;/h2&gt;

&lt;p&gt;The second-most-common finding is themes and customizations that render the Add to Cart action as a styled div with an onClick handler instead of a real HTML button. Visually it looks exactly the same. With a keyboard, the user cannot tab to it. With a screen reader, the device says nothing — there is no role to announce. The store has effectively put the most important action behind a wall.&lt;/p&gt;

&lt;p&gt;This typically shows up in heavily customized themes, in page builders like Elementor or Divi when the developer used a generic "container" element with a click action, and in custom shop layouts that override the WooCommerce loop template. WCAG criterion 2.1.1 Keyboard requires every interactive element to be operable with a keyboard, and 4.1.2 Name, Role, Value requires it to announce its role to assistive technology. A div with onClick fails both, as our &lt;a href="https://dev.to/blog/react-tutorial-accessibility-mistakes/"&gt;React tutorial accessibility post&lt;/a&gt; explains for a related framework, but the underlying problem is identical in WordPress and PHP.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: ask whoever built the site to change every Add to Cart, Buy Now, Apply Coupon, and Checkout element to use a real  or &lt;a&gt; tag with the right href. If you are using Elementor or Divi, switch the affected element from a "Click box" or "Container with link" to the dedicated "Button" element, which renders semantic markup. If you are using a Shopify-style theme that ships its own product card template, the line you are looking for in the template is usually the loop start in &lt;code&gt;content-product.php&lt;/code&gt; — bring it to your developer with a screenshot of the keyboard test.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Coupon code, quantity, and shipping fields with no labels
&lt;/h2&gt;

&lt;p&gt;WooCommerce checkout pages are generated by a mix of core templates, the active theme, and any plugins that add fields (subscription managers, shipping calculators, gift wrap, address validators). Every one of those layers can introduce a form field that is missing a label or has a label associated only by visual proximity. The result is a checkout where a screen reader user hears "edit, edit, edit, edit" instead of "Email, edit", "Phone, edit", "Address line 1, edit".&lt;/p&gt;

&lt;p&gt;The other failure mode here is fields that have visible labels in the design but use placeholder text instead of a real  element. The placeholder disappears as soon as the user types, so a user who pauses to switch tabs comes back to a blank field with no indication of what it was for. This is also what happens to a user with cognitive disabilities or a non-native speaker who needs to re-read the label after starting to fill it in.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: install a checkout audit. We use the WAVE browser extension for this — load the cart, then load the checkout, and look for the orange "missing form label" markers. For each one, look up the plugin or theme component that owns the field and either configure a label in its settings panel (most plugins have one) or replace the plugin with one that ships labels by default. Our deeper &lt;a href="https://dev.to/blog/accessible-forms-guide/"&gt;accessible forms guide&lt;/a&gt; covers the patterns that pass screen reader testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The mini-cart and ajax updates that nobody hears
&lt;/h2&gt;

&lt;p&gt;A WooCommerce store with ajax-enabled cart updates is a great experience for sighted users — click Add to Cart, see the cart number tick up in the header, keep shopping. For a screen reader user, the same interaction makes no sound. The DOM has updated, the visual count has changed, but no live region announced "1 item added to cart" so the user has no idea anything happened.&lt;/p&gt;

&lt;p&gt;This is a violation of WCAG 4.1.3 Status Messages. It is also one of the most consistent reasons that screen reader users describe ecommerce sites as "broken", because the entire purchase flow is a sequence of silent state changes. The widely deployed WooCommerce themes from Storefront, Astra, and OceanWP all ship without an aria-live region around the mini-cart, and most theme customizations do not add one.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: ask your developer to add a small live region to the cart UI. The exact change is one line of HTML — a wrapper element around the mini-cart count or notification area with &lt;code&gt;aria-live="polite"&lt;/code&gt; and &lt;code&gt;aria-atomic="true"&lt;/code&gt;. After the change, every cart update should be announced. This same technique applies to coupon application messages, stock-status updates on product pages, and the "successfully added to wishlist" toasts that show up after a click. If you are working with a developer, our post on &lt;a href="https://dev.to/blog/automate-accessibility-fixes-github-action/"&gt;automating accessibility fixes in CI&lt;/a&gt; covers how to keep regressions from coming back the next time the theme is updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Color contrast that looks fine on the homepage and fails on every product page
&lt;/h2&gt;

&lt;p&gt;WooCommerce stores tend to be designed once for the homepage and then forgotten on the templated product, category, and checkout pages. The homepage gets the brand colors set deliberately. The product page inherits whatever defaults the theme shipped with — light grey "Sale" badges on white backgrounds, light brand-colored "Add to Cart" buttons whose label fails contrast, "Free shipping over $50" banners in pastel beige.&lt;/p&gt;

&lt;p&gt;Color contrast is one of the few WCAG criteria that automated tools detect reliably. WCAG 2.2 Level AA requires a contrast ratio of 4.5:1 for normal-size text and 3:1 for large text and meaningful graphical elements. The most-cited demand-letter contrast failures we see in WooCommerce stores are sale-price badges (often pink or red text on a pastel background), discount banners in light theme accent colors, and the small print under product cards that describes shipping eligibility.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: run a contrast pass on a product page, a category page, and the cart page using a tool like the Chrome DevTools color picker or the WebAIM Contrast Checker. For each failure, adjust the color in the theme customizer rather than in CSS — most modern WooCommerce themes expose primary, accent, and badge colors directly. We have a longer walkthrough in our &lt;a href="https://dev.to/blog/color-contrast-guide/"&gt;color contrast guide&lt;/a&gt; if you want screenshots of the specific values that pass and fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Plugins that override accessible defaults
&lt;/h2&gt;

&lt;p&gt;The WooCommerce plugin ecosystem is enormous, and plugin authors face no centralized accessibility review. A few patterns show up over and over: subscription plugins that render their own checkout fields without labels, currency switchers that use clickable spans instead of select elements, product filter widgets that nest five layers of div onClick handlers, and payment gateways that load iframes with form fields the parent page cannot reach for keyboard or screen reader users. The store owner installs each plugin to solve a real problem and ends up with a stack of accessibility regressions they did not know they were buying.&lt;/p&gt;

&lt;p&gt;The biggest culprits we see across audits are filtering and faceted search plugins (WooCommerce Product Filter, BeRocket, Annasta), AJAX cart plugins that override the default cart and lose the live region, and overlay widgets like accessiBe and UserWay that the owner installed thinking they would help. Our &lt;a href="https://dev.to/blog/why-accessibility-overlays-dont-work/"&gt;why overlay widgets do not work&lt;/a&gt; post covers that last category in detail, including the specific demand letters in which overlay widgets were the named defendant.&lt;/p&gt;

&lt;p&gt;What to do without rewriting the theme: before installing any new WooCommerce plugin, search the plugin's name plus "accessibility" and check whether the support forum or recent reviews flag any issues. If you have already installed a long stack, audit the plugins that touch checkout and product display first — those are the pages that ship in demand letters. Replace any plugin that fails a basic keyboard test (tab through every interactive element on the page; can you reach all of them and operate them with Enter or Space?) with an alternative that passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we recommend, in order
&lt;/h2&gt;

&lt;p&gt;If you run a WooCommerce store and you have read this far, the order we recommend is: fix the alt text on product images, replace any div-as-button with a real button or link, audit the checkout fields for missing labels, add the live region around the mini-cart, run a contrast check on three template pages, and uninstall any plugin that introduces an overlay widget. None of these require rewriting your theme. Each one closes a specific failure that has shown up in a specific demand letter against a small WooCommerce store in the last twelve months.&lt;/p&gt;

&lt;p&gt;If you want a starting point that covers the rest of the site, our &lt;a href="https://dev.to/blog/wordpress-accessibility-guide/"&gt;WordPress accessibility guide&lt;/a&gt; walks through the same audit at the platform level, and the &lt;a href="https://dev.to/checklist/wordpress/"&gt;WordPress WCAG audit checklist&lt;/a&gt; collects the recurring failures we see across themes, page builders, and form plugins in a structured criterion-by-criterion format. If you are running an e-commerce checkout flow and want to be sure your specific checkout passes, our &lt;a href="https://dev.to/blog/accessible-ecommerce-checkout-guide/"&gt;accessible e-commerce checkout guide&lt;/a&gt; goes deeper into the checkout-specific patterns. And if you have already received a demand letter or are worried about getting one, our post on &lt;a href="https://dev.to/blog/ada-lawsuits-small-business/"&gt;ADA lawsuits and small business&lt;/a&gt; covers the realistic scope of risk and what kind of response the demand letter is actually asking for.&lt;/p&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/wordpress-accessibility-guide/"&gt;WordPress Accessibility Guide&lt;/a&gt; -- the platform-level audit that sits underneath every WooCommerce store&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/accessible-ecommerce-checkout-guide/"&gt;Accessible E-commerce Checkout Guide&lt;/a&gt; -- a deeper walkthrough of the checkout flow patterns that show up in demand letters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/ada-lawsuits-small-business/"&gt;ADA Lawsuits and Small Business&lt;/a&gt; -- realistic scope of e-commerce demand-letter risk and how to think about response cost&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What Is a VPAT? A Plain-English Guide for B2B SaaS and Service Providers</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Mon, 04 May 2026 05:00:36 +0000</pubDate>
      <link>https://dev.to/agentkit/what-is-a-vpat-a-plain-english-guide-for-b2b-saas-and-service-providers-57d0</link>
      <guid>https://dev.to/agentkit/what-is-a-vpat-a-plain-english-guide-for-b2b-saas-and-service-providers-57d0</guid>
      <description>&lt;p&gt;The first time a prospect asks "Can you send us your VPAT?" most B2B founders react one of two ways. Either they panic and forward the email to a developer, or they reply "What's a VPAT?" and lose the deal to a competitor who already had one ready. Neither is necessary. A VPAT is a template, not a certification, and any company can produce one if they understand the document, do the underlying accessibility work honestly, and resist the temptation to overstate what their product actually supports.&lt;/p&gt;

&lt;p&gt;This guide explains what a VPAT is, what an ACR is, when you genuinely need one, when you don't, what it costs to produce a credible report, and the most common mistakes that turn a VPAT from a competitive advantage into a legal liability. None of this is legal advice; if you are about to publish a VPAT or sign a contract that references one, talk to an accessibility-experienced attorney for your jurisdiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  VPAT vs ACR: the names matter
&lt;/h2&gt;

&lt;p&gt;The Information Technology Industry Council (ITI) publishes the &lt;strong&gt;Voluntary Product Accessibility Template&lt;/strong&gt; ("VPAT"), which is a blank document with a standard structure. When you fill out a VPAT with your product's actual conformance to accessibility standards, the completed document is called an &lt;strong&gt;Accessibility Conformance Report&lt;/strong&gt; ("ACR"). In practice, almost everyone uses "VPAT" to mean both the template and the completed report, and procurement teams will accept either term. The technically correct phrasing is "we have a VPAT-format ACR for our product."&lt;/p&gt;

&lt;p&gt;The current version of the VPAT is &lt;strong&gt;VPAT 2.5 Rev&lt;/strong&gt;, published in 2024, and it covers four standards in a single template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WCAG 2.0&lt;/strong&gt;, &lt;strong&gt;WCAG 2.1&lt;/strong&gt;, and (in the most recent revisions) &lt;strong&gt;WCAG 2.2&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section 508&lt;/strong&gt; (Revised 508 Standards from 2017, which are the US federal procurement requirements)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EN 301 549&lt;/strong&gt; (the European harmonized standard referenced by the European Accessibility Act)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to fill out every section. The template comes in four editions: WCAG-only, 508-only, EN-only, and INT (international, which covers all three). Pick the edition that matches what your buyer is actually asking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually needs a VPAT?
&lt;/h2&gt;

&lt;p&gt;The honest answer is "fewer companies than you think, but more than was true five years ago." A VPAT is needed in three buyer contexts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;US federal government, state government, and public universities.&lt;/strong&gt; Section 508 requires federal agencies to procure information and communication technology that conforms to specified accessibility standards, and most large state agencies and public universities have adopted similar requirements. If you sell software, training, content, or services to these buyers, expect a VPAT request as part of the standard procurement questionnaire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;European public sector and (increasingly) European enterprise.&lt;/strong&gt; EN 301 549 is the EU's harmonized accessibility standard, and it is referenced by the &lt;strong&gt;European Accessibility Act&lt;/strong&gt; (effective June 28, 2025) for many private-sector products and services as well. Public sector buyers in the EU have required EN 301 549 conformance evidence for years, and enterprise procurement teams are increasingly catching up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise buyers in regulated industries.&lt;/strong&gt; Banks, insurance companies, healthcare providers, and other organizations subject to ADA Title III obligations or sector-specific accessibility rules have started asking vendors for VPATs as part of risk management. They want documentation that the tools they buy will not create new accessibility barriers for their employees or customers.&lt;/p&gt;

&lt;p&gt;If you only sell to small businesses, individual professionals, or consumers, you almost certainly do not need a VPAT today. You still need to make your product accessible (the EAA, ADA, AODA, and other laws apply directly to you regardless of whether anyone asks for a VPAT), but you don't need the formal report.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a VPAT is not
&lt;/h2&gt;

&lt;p&gt;A VPAT is &lt;strong&gt;not a certification&lt;/strong&gt;. No third party signs off on a VPAT. The vendor (you) writes the report and stands behind it. This is why a VPAT prepared by an accessibility consultant has more credibility than one written internally by a sales engineer with no accessibility background. The signature line says "prepared by," not "certified by."&lt;/p&gt;

&lt;p&gt;A VPAT is &lt;strong&gt;not a guarantee of legal compliance&lt;/strong&gt;. It documents your product's conformance to specific success criteria at a moment in time. It is evidence of effort, not a defense against a lawsuit. A plaintiff can still allege that your product violates the ADA, AODA, or EAA regardless of what your VPAT says.&lt;/p&gt;

&lt;p&gt;A VPAT is &lt;strong&gt;not a one-time deliverable&lt;/strong&gt;. If your product changes (which it does, constantly), the VPAT becomes stale. Most buyers expect the VPAT to be updated at least annually, and some procurement contracts require notification when material accessibility regressions are introduced.&lt;/p&gt;

&lt;p&gt;A VPAT is &lt;strong&gt;not the same as an audit&lt;/strong&gt;. A VPAT is the report. An audit is the underlying work that makes the report credible. If you fill out a VPAT without actually testing your product against each WCAG success criterion, you are guessing, and a sophisticated buyer will catch you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four conformance levels you'll see in a VPAT
&lt;/h2&gt;

&lt;p&gt;For each WCAG success criterion (or each Section 508 / EN 301 549 requirement), the VPAT asks you to mark conformance using one of these terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supports&lt;/strong&gt;: The product fully meets the criterion. Use this only when you have actually tested and the result is conformant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partially Supports&lt;/strong&gt;: Most of the product meets the criterion, but there are exceptions. You must describe the exceptions in the Remarks column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does Not Support&lt;/strong&gt;: The product does not meet the criterion. You must describe the gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Applicable&lt;/strong&gt;: The criterion does not apply to your product (for example, audio description criteria do not apply to a product that contains no audio).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Evaluated&lt;/strong&gt; (Level AAA only): You did not test against this criterion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most common mistake on bad VPATs is marking everything as "Supports" without doing the testing. A buyer who has done a few procurement reviews will spot this immediately, because no real product fully supports every WCAG criterion. A credible VPAT will have a mix of Supports, Partially Supports, and Does Not Support, with honest Remarks that describe the gaps and any planned remediation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs to produce a credible VPAT
&lt;/h2&gt;

&lt;p&gt;The cost depends on the size of your product, the experience of the team doing the work, and how much accessibility work you have already done. As a rough industry range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal VPAT, small SaaS product, with prior accessibility testing&lt;/strong&gt;: 20-60 hours of senior engineer time. Often $5,000-$15,000 in opportunity cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consultant-prepared VPAT, small to mid-size product&lt;/strong&gt;: $5,000-$25,000, including the underlying audit work needed to produce credible Remarks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consultant-prepared VPAT, large enterprise product&lt;/strong&gt;: $25,000-$100,000+, including manual testing across multiple browser and assistive technology combinations, screen-reader walkthroughs, and detailed remediation planning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest VPATs you can buy on the open market are usually $1,500-$3,000 and consist of a consultant filling out the template based on a brief automated scan. These are worth almost nothing - they will fail any sophisticated procurement review, and they expose you to legal risk because they assert conformance you have not actually verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic VPAT workflow for a B2B SaaS company
&lt;/h2&gt;

&lt;p&gt;If you are a small or mid-size SaaS company facing your first VPAT request, here is the workflow that usually works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick the edition that matches the buyer's question.&lt;/strong&gt; If a US federal agency asks, use the 508 or INT edition. If a European public-sector buyer asks, use the EN or INT edition. If a US enterprise asks for a "WCAG VPAT," the WCAG edition is fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run an honest audit first.&lt;/strong&gt; Either hire a third-party accessibility consultant to do a manual audit of your product against WCAG 2.2 Level AA (most VPAT requests now ask for 2.2), or have a senior engineer with documented accessibility training do the same work. Automated scans alone are not sufficient - they catch about 30-40% of WCAG failures, and the most expensive failures (keyboard traps, missing labels, broken focus management) are mostly in the part automated tools miss.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document gaps in the Remarks column.&lt;/strong&gt; A VPAT that says "Partially Supports" with a Remark like "Custom date picker does not announce selected date to screen readers; remediation planned for Q3 2026" is far more credible than a VPAT that says "Supports" for everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get sign-off from someone who could be deposed.&lt;/strong&gt; The signature line on a VPAT is not theoretical. If a customer sues over an accessibility issue you marked as "Supports" without testing, your signed VPAT becomes evidence in the case. Make sure the person signing has actually reviewed the underlying audit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publish the VPAT or share it on request.&lt;/strong&gt; Some companies publish their VPAT on a public accessibility page (alongside their accessibility statement). Others share it only on request under NDA. Both are acceptable; the choice usually depends on whether the VPAT exposes details about your product roadmap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-audit and re-publish at least annually.&lt;/strong&gt; Every major release should include an accessibility regression check, and the VPAT should be updated whenever you find a material change in conformance.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes that hurt deals (or cause lawsuits)
&lt;/h2&gt;

&lt;p&gt;A few patterns turn up over and over in failing or risky VPATs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marking everything as Supports.&lt;/strong&gt; Already covered, but worth repeating. This is the single biggest red flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing up product editions.&lt;/strong&gt; A VPAT for "Acme SaaS" is meaningless if Acme SaaS has a free tier, a Pro tier, and a mobile app, each with different accessibility behavior. Specify which edition the VPAT covers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citing automated scan results as if they were conformance evidence.&lt;/strong&gt; Automated tools give you a starting point, not a conformance claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting third-party components.&lt;/strong&gt; If your product embeds Stripe Checkout, a Calendly widget, an Intercom chat, or a video player, the accessibility of those components affects your VPAT. Either test them, document their conformance separately, or note the gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using a stale template.&lt;/strong&gt; VPAT 1.x and 2.0 are out of date. If a procurement team receives a VPAT 1.3 in 2026, they will ask for a current one and your sale slows down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the VPAT as a marketing document.&lt;/strong&gt; A VPAT that reads like marketing copy is suspicious. A VPAT that reads like a technical compliance document is reassuring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How a VPAT relates to your accessibility statement
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;accessibility statement&lt;/strong&gt; is a short, public-facing page on your website that describes your conformance target, your approach to accessibility, known limitations, and how users can request help or report issues. The European Accessibility Act and the 2024 DOJ Title II rule both expect public-facing organizations to publish one.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;VPAT/ACR&lt;/strong&gt; is a longer, technical document used in procurement that documents conformance criterion-by-criterion against a specific standard.&lt;/p&gt;

&lt;p&gt;Both should agree with each other. If your accessibility statement says you target WCAG 2.2 Level AA and your VPAT says you do not yet support several Level A criteria, you have a credibility problem. The fix is to update both documents at the same time: scope the accessibility statement to known conformance, list the gaps, and produce a VPAT that matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a VPAT is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Not every accessibility question deserves a VPAT response. If a customer asks "Is your product accessible?", the right answer is usually a brief description of your accessibility commitments and a link to your public accessibility statement. Sending an unsolicited 40-page VPAT to a small business buyer is overkill and can actually slow the deal down. Save the VPAT for procurement teams that specifically ask for one.&lt;/p&gt;

&lt;p&gt;If the buyer is asking because they have a specific user with a specific assistive technology need, a focused conversation about that user's workflow is far more useful than a generic VPAT. "Can a JAWS user complete the entire onboarding flow without help?" is a question your VPAT should answer in a Remark, but the buyer may want a live walkthrough as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bottom line
&lt;/h2&gt;

&lt;p&gt;A VPAT is a credibility tool, not a compliance tool. It does not make your product accessible. It documents the accessibility work you have already done. The fastest way to produce a useful VPAT is to make your product genuinely conformant to WCAG 2.2 Level AA, hire a qualified consultant or train a senior engineer to do the audit, fill out the template honestly, and update it on every major release.&lt;/p&gt;

&lt;p&gt;The fastest way to produce a harmful VPAT is to mark everything as Supports without testing, send it to a procurement team that knows what they are looking at, and lose the deal. Or worse, win the deal and then face a lawsuit when the buyer's users hit the accessibility issues your VPAT claimed didn't exist.&lt;/p&gt;

&lt;p&gt;If you are starting from zero, the highest-leverage steps are: (1) commission a manual WCAG 2.2 Level AA audit of your most-used flows, (2) fix the high-impact issues, (3) publish an accurate accessibility statement, and (4) prepare a VPAT that matches. Skipping the first three and producing a VPAT in isolation is the worst-case path.&lt;/p&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/accessibility-statement-guide/"&gt;How to Write an Accessibility Statement (Plain-English Template)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/audit-vs-statement-which-protects-you/"&gt;Audit vs Statement: Which One Actually Protects You?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/doj-title-ii-deadline-passed-what-to-do/"&gt;DOJ Title II Deadline Passed: What to Do Now&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Lighthouse Score Is 95. You Could Still Get a Demand Letter Tomorrow.</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sun, 03 May 2026 05:00:51 +0000</pubDate>
      <link>https://dev.to/agentkit/your-lighthouse-score-is-95-you-could-still-get-a-demand-letter-tomorrow-416n</link>
      <guid>https://dev.to/agentkit/your-lighthouse-score-is-95-you-could-still-get-a-demand-letter-tomorrow-416n</guid>
      <description>&lt;p&gt;A 95 in Chrome's Lighthouse accessibility audit is the number small-business owners point at when their developer asks whether the site is okay. It is the number agencies put in their delivery report. And it is the number that, three months later, sits next to a demand letter on the same desk.&lt;/p&gt;

&lt;p&gt;The gap is not Lighthouse being broken. Lighthouse is doing exactly what it claims to do — run an automated scan against a defined subset of the WCAG criteria. The gap is between what an automated scan can prove and what a plaintiffs' firm needs to prove. And that gap is wide enough that the relationship between scanner score and lawsuit risk is much weaker than the score makes it look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What scanners are actually testing
&lt;/h2&gt;

&lt;p&gt;axe-core, the engine behind Lighthouse and most browser-extension accessibility tools, runs around 90 automated rules. Every one is a binary check that can be answered by reading the rendered HTML and CSS: does this image have an alt attribute, does this input have an associated label, is this color contrast ratio above the threshold, is the page language declared.&lt;/p&gt;

&lt;p&gt;Deque, the company behind axe-core, publishes a clear figure on this: automated tools catch approximately 30 to 40 percent of WCAG issues. The remaining 60 to 70 percent require a human running the page with a keyboard, a screen reader, and a brain.&lt;/p&gt;

&lt;p&gt;The 30 to 40 percent that scanners catch is real, useful work. A site with twenty axe violations is genuinely worse off than a site with two. The problem is that the rest of the iceberg is what plaintiffs' firms cite in demand letters, because their tester is a human, not an automated scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alt text exists, but says nothing
&lt;/h2&gt;

&lt;p&gt;The Lighthouse rule for images is whether the alt attribute is present. It is not whether the alt text is meaningful. Every one of these passes the scan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;img src="hero.jpg" alt="hero"&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;img src="DSC_4271.jpg" alt="DSC_4271.jpg"&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;img src="bride.jpg" alt="image"&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;img src="product.jpg" alt=""&amp;gt;&lt;/code&gt; on a product page where the image is the only thing identifying the item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A blind shopper trying to figure out which blue dress on the page is the one her friend wore last weekend hears "image, image, image, image" and bounces. That is the experience cited in dozens of recent demand letters against e-commerce stores. The scanner gave the site a clean report.&lt;/p&gt;

&lt;p&gt;What to do: walk through your site reading the alt text out loud. If it would not allow a stranger to picture the image, rewrite it. For decorative images, use &lt;code&gt;alt=""&lt;/code&gt; deliberately. For product images, the alt text should at minimum identify the product and its key visual features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headings are nested, but not in reading order
&lt;/h2&gt;

&lt;p&gt;Lighthouse checks that your headings do not skip levels (no jumping from H2 to H4) and that the page has at least one H1. It does not check whether the headings tell a coherent story when read in order, which is how a screen-reader user navigates an unfamiliar page.&lt;/p&gt;

&lt;p&gt;The most common failure: the H1 says "About Us", the next H2 says "Our Mission", and then mid-page there is an H2 that says "Get a Free Quote" because the design template put a CTA there. A screen-reader user pulling up the heading list to scan the page sees an "About Us" page that suddenly offers a quote, with no clue what is being quoted.&lt;/p&gt;

&lt;p&gt;What to do: install the WAVE browser extension (free) and click the "Structure" tab. It shows the heading outline as a nested list. Read it out loud. If the outline does not summarize the page in a way that would make sense to a stranger, your headings need to be rewritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form labels exist, but do not describe the field
&lt;/h2&gt;

&lt;p&gt;The Lighthouse rule for form inputs is whether each input has an associated &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;, an &lt;code&gt;aria-label&lt;/code&gt;, or an &lt;code&gt;aria-labelledby&lt;/code&gt;. It does not check whether the label text actually describes the field. The following all pass the scan and fail any human test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A field labeled "Field" with placeholder "Enter your email"&lt;/li&gt;
&lt;li&gt;A series of fields labeled "Item 1", "Item 2", "Item 3"&lt;/li&gt;
&lt;li&gt;A required birth-date field labeled "Date" with no format hint&lt;/li&gt;
&lt;li&gt;A faceted-search interface with six different search boxes all labeled "Search"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screen-reader user hears "edit text, search, edit text, search" and cannot tell which one is the keyword search and which are the date-range filters. They abandon the task.&lt;/p&gt;

&lt;p&gt;What to do: read your form labels in isolation, without looking at the rest of the form. If a label would not tell a stranger what to enter, rewrite it. Mark required fields in the label text ("Email (required)") rather than relying on a red asterisk that might not be announced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard navigation appears to work, until you actually try it
&lt;/h2&gt;

&lt;p&gt;Lighthouse cannot run your page with a keyboard. It detects a few keyboard hazards (missing skip link, positive &lt;code&gt;tabindex&lt;/code&gt;), but it cannot tell whether your custom dropdown opens with Enter, whether your modal traps focus correctly, whether the focus indicator is visible against your background, or whether the user can escape your cookie banner without a mouse.&lt;/p&gt;

&lt;p&gt;The recurring failure modes we find in audits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;-based dropdown that opens on click but ignores Enter, Space, and arrow keys&lt;/li&gt;
&lt;li&gt;A modal that opens but leaves focus on the button behind it, so a screen-reader user does not know it is open&lt;/li&gt;
&lt;li&gt;A modal that opens but does not trap focus, so Tab takes the user into the hidden page underneath&lt;/li&gt;
&lt;li&gt;A focus indicator deliberately removed by a developer because "it looked ugly"&lt;/li&gt;
&lt;li&gt;Hamburger-menu navigation on mobile that opens but cannot be closed without a mouse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What to do: click into the URL bar of any page on your site and press Tab repeatedly until you have visited every interactive element. Ask: can I see where focus is? When I press Enter on a button, does something happen? When a modal opens, can I get back out without my mouse? You do not need a screen reader. A keyboard is enough. Our &lt;a href="https://dev.to/blog/keyboard-navigation-testing/"&gt;keyboard navigation testing guide&lt;/a&gt; walks through it step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form errors flash on screen but never reach a screen reader
&lt;/h2&gt;

&lt;p&gt;When a user submits a form with an invalid email, the standard pattern is for a red error message to appear next to the field. Lighthouse does not test whether that error is actually announced to a screen-reader user. It checks the labels and the contrast, but the bridge between the two — whether assistive technology is told the error happened — is not in scope for any automated scan.&lt;/p&gt;

&lt;p&gt;The result: a blind user submits the form, hears nothing, and has no idea why nothing happened. They might submit again. They might leave.&lt;/p&gt;

&lt;p&gt;What to do: every error message needs to be announced. The simplest pattern is to render the error inside an element with &lt;code&gt;role="alert"&lt;/code&gt; or inside a container marked &lt;code&gt;aria-live="polite"&lt;/code&gt; so the screen reader picks up the change. If you are not in a position to write the markup yourself, this is a specific request to send your developer or platform support team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color contrast is fine on solid backgrounds, broken on photos
&lt;/h2&gt;

&lt;p&gt;Lighthouse measures color contrast by reading the CSS foreground and background colors. It cannot measure the actual contrast of white text rendered on top of a hero photograph, because the background color in CSS is "transparent" and the photograph is not part of the contrast calculation. Same problem with gradient backgrounds, video backgrounds, and CSS background-image patterns.&lt;/p&gt;

&lt;p&gt;We see this in nearly every photographer, restaurant, and hospitality audit: the hero "Book Now" button reads as 1.5:1 against a section of the background image — a hard failure under WCAG 1.4.3. Lighthouse reports zero contrast violations.&lt;/p&gt;

&lt;p&gt;What to do: test each page with text over an image manually. The WAVE browser extension and the free WhoCanUse contrast tool both let you check with custom backgrounds. The standard fixes are: add a darkening overlay between image and text, add a text shadow, swap to a solid-color band behind the text, or move the text out of the image area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom widgets pass automation, fail screen readers
&lt;/h2&gt;

&lt;p&gt;Calendars, date pickers, accordions, tabs, autocomplete dropdowns, modal dialogs, drag-and-drop uploaders, and rich-text editors are where automated and human results diverge most sharply. axe-core detects a few specific patterns, but it cannot test whether the widget actually works for an assistive-technology user. The widget can be fully scan-clean and fully unusable.&lt;/p&gt;

&lt;p&gt;The most consequential case is the booking-and-checkout flow. We have audited dozens of small-business booking systems where the date picker passes Lighthouse, the time picker passes Lighthouse, the contact-info form passes Lighthouse, and the whole flow is unusable with a screen reader because focus order is broken between steps and the success confirmation is a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with no live region.&lt;/p&gt;

&lt;p&gt;What to do: identify your one or two most important user journeys (booking, checkout, sign-up, contact) and test each one end to end with the keyboard alone. If you can complete the journey without touching the mouse, that is a strong baseline. The next step up is the same journey with a screen reader — on Mac, VoiceOver is built in (Cmd+F5). An hour to learn the basics is worth more than another hundred Lighthouse runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What scanners do not look at all
&lt;/h2&gt;

&lt;p&gt;Lighthouse audits the rendered HTML of the page it is told to scan. It does not audit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDFs linked from the page (your menu, your bulletin, your white paper, your brochure)&lt;/li&gt;
&lt;li&gt;Embedded videos and their captions&lt;/li&gt;
&lt;li&gt;Third-party widgets loaded after the audit (chat widgets, scheduling embeds, payment forms, social-media feeds)&lt;/li&gt;
&lt;li&gt;Pages behind a login (member portals, dashboards, account settings)&lt;/li&gt;
&lt;li&gt;Mobile-specific layouts when the audit is run in desktop mode (the default)&lt;/li&gt;
&lt;li&gt;Email templates sent to the same users who visit the site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is its own audit surface and each is regularly cited in demand letters. A small business with a perfect Lighthouse score on its homepage and a 47-page scanned-PDF menu linked from that homepage has a serious accessibility problem and a clean scanner report.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is the right number?
&lt;/h2&gt;

&lt;p&gt;The right number is not a single Lighthouse score. It is a small set of complementary signals. The combination we recommend for non-developers:&lt;/p&gt;

&lt;p&gt;A Lighthouse or WAVE automated scan to catch the obvious 30 to 40 percent. A keyboard-only walkthrough of your top three user journeys. A read-out-loud test of your alt text and form labels. A check of every PDF and embedded video for caption and tagging. And, critically, an accessibility statement on your site documenting your conformance commitment, your contact channel for accommodations, and the date of your last review — which is the single most consistent thing demand-letter responses cite as evidence of good faith.&lt;/p&gt;

&lt;p&gt;If you want to go further, our &lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;five-minute accessibility audit&lt;/a&gt; walks through the keyboard test step by step. Our &lt;a href="https://dev.to/blog/why-accessibility-overlays-dont-work/"&gt;why accessibility overlays don't work&lt;/a&gt; post covers a related "false sense of security" pattern. And our internal post-mortem on &lt;a href="https://dev.to/blog/27-to-1-fixing-our-own-accessibility-violations/"&gt;finding 27 issues a scanner missed on our own site&lt;/a&gt; is the case study we point to when a client asks why 95 is not the answer.&lt;/p&gt;

&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;Five-Minute Accessibility Audit&lt;/a&gt; -- a step-by-step keyboard walkthrough you can do today&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/why-accessibility-overlays-dont-work/"&gt;Why Accessibility Overlays Don't Work&lt;/a&gt; -- another false-sense-of-security pattern, and the lawsuits that followed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/27-to-1-fixing-our-own-accessibility-violations/"&gt;27 to 1: Fixing Our Own Accessibility Violations&lt;/a&gt; -- what an automated scan said about our site versus what we actually found&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hamburger Menu Accessibility: Why Your Mobile Nav Locks Out Customers (And How to Fix It)</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sun, 03 May 2026 05:00:31 +0000</pubDate>
      <link>https://dev.to/agentkit/hamburger-menu-accessibility-why-your-mobile-nav-locks-out-customers-and-how-to-fix-it-2e3d</link>
      <guid>https://dev.to/agentkit/hamburger-menu-accessibility-why-your-mobile-nav-locks-out-customers-and-how-to-fix-it-2e3d</guid>
      <description>&lt;p&gt;Open a small-business website on your phone and tap the three lines in the corner. A menu slides in. You tap a link. The page loads. That is what is supposed to happen.&lt;/p&gt;

&lt;p&gt;Now try the same thing without using your hands. Or with the screen reader your phone already has built in. Or with the screen zoomed to 200%. The hamburger menu that worked perfectly with a fingertip suddenly traps you, hides itself, or refuses to close. Roughly four out of five mobile sites we have tested have at least one accessibility issue inside that menu, and it is the single most common pattern that produces ADA demand letters and EAA complaints in 2026.&lt;/p&gt;

&lt;p&gt;The good news is that the fixes are concrete, mostly invisible to the visual design, and almost all of them can be applied from inside your website builder's settings without editing a line of code. Here is what is going wrong on most hamburger menus, why each issue matters, and what you can actually do about it today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the hamburger menu is a litigation magnet
&lt;/h2&gt;

&lt;p&gt;Three reasons make this control a disproportionate target.&lt;/p&gt;

&lt;p&gt;First, every visitor encounters it. Unlike a checkout flow or a contact form that only some users reach, the navigation is the gateway to the entire site. If it fails, the whole site fails for that user.&lt;/p&gt;

&lt;p&gt;Second, the hamburger pattern is custom-built on most platforms. WordPress themes, Webflow templates, Squarespace styles, and Shopify themes each implement the menu slightly differently, and most implementations were copied from a template author who optimized for visual flair rather than assistive-technology support. Any platform-level accessibility fix Shopify or Squarespace ships does not retroactively patch your live site.&lt;/p&gt;

&lt;p&gt;Third, automated scanners flag the failures clearly. axe DevTools, Lighthouse, and WAVE all surface hamburger-menu issues with screenshots and code snippets, which is exactly the kind of evidence that ADA plaintiff law firms attach to demand letters. A site that fails the menu typically fails it on every page, multiplying the violation count.&lt;/p&gt;

&lt;p&gt;If you sell digital products to consumers in the EU and you fall under the European Accessibility Act, your obligations took effect in June 2025. A demonstrably broken navigation is one of the easiest issues for a complaint to surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 1: The trigger button has no label
&lt;/h2&gt;

&lt;p&gt;The classic hamburger icon is three horizontal lines and nothing else. Visually that works because you trained your users to recognize it. To a screen reader, that button is announced as "button" with no further description. Users hear "button" and have no way to know the button opens a menu.&lt;/p&gt;

&lt;p&gt;The fix is a single attribute that takes a screen-reader user from confused to oriented: &lt;code&gt;aria-label="Open menu"&lt;/code&gt;. Every major website builder lets you set this from the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Squarespace 7.1&lt;/strong&gt;: Site Styles &amp;gt; Header &amp;gt; Menu Trigger &amp;gt; Accessibility label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wix&lt;/strong&gt;: click the burger element &amp;gt; Settings &amp;gt; ARIA label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webflow&lt;/strong&gt;: select the menu button element &amp;gt; Element Settings (D shortcut) &amp;gt; Accessibility &amp;gt; Aria label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify&lt;/strong&gt;: most themes have a translation file (&lt;code&gt;locales/en.default.json&lt;/code&gt;) with an &lt;code&gt;accessibility.menu&lt;/code&gt; key. Update the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress (most block themes)&lt;/strong&gt;: Site Editor &amp;gt; Header pattern &amp;gt; select the navigation block &amp;gt; Advanced &amp;gt; HTML element &amp;gt; add &lt;code&gt;aria-label&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify your fix with the screen reader on your phone. iOS users can enable VoiceOver in Settings &amp;gt; Accessibility &amp;gt; VoiceOver. Android users have TalkBack in Settings &amp;gt; Accessibility. Tap the hamburger and you should hear "Open menu, button" instead of "button".&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 2: The trigger does not announce open or closed state
&lt;/h2&gt;

&lt;p&gt;Even with a label, the trigger button needs to tell screen readers whether the menu is currently open or closed. The attribute that does this is &lt;code&gt;aria-expanded&lt;/code&gt;, which should toggle between &lt;code&gt;"true"&lt;/code&gt; and &lt;code&gt;"false"&lt;/code&gt; when the user taps the trigger.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;aria-expanded&lt;/code&gt; is missing or stuck on a fixed value, screen-reader users hear "Open menu, button" both when the menu is closed and when it is already open. They tap the button repeatedly trying to figure out whether it worked.&lt;/p&gt;

&lt;p&gt;This fix is more invasive in some platforms, because the toggle behaviour is governed by the theme's JavaScript. Workarounds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webflow&lt;/strong&gt;: set the navigation menu element to use the built-in Nav component (not a custom interaction), which manages &lt;code&gt;aria-expanded&lt;/code&gt; automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Squarespace and Wix&lt;/strong&gt;: their default mobile menus already toggle &lt;code&gt;aria-expanded&lt;/code&gt; correctly. The issue appears mainly on heavily customized templates. If you injected a custom hamburger via Code Block, replace it with the platform default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify themes&lt;/strong&gt;: every Online Store 2.0 theme released since 2022 handles this in the &lt;code&gt;header.liquid&lt;/code&gt; template. If you are on an older theme, this is a strong reason to upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt;: the core Navigation block manages &lt;code&gt;aria-expanded&lt;/code&gt; correctly. Custom menu plugins (UberMenu, Max Mega Menu) sometimes do not. Check by inspecting the trigger in your phone browser's DevTools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Issue 3: The menu is a div, not a real navigation landmark
&lt;/h2&gt;

&lt;p&gt;A menu inside a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is invisible to screen-reader users who navigate by landmark (the rotor on iOS, gestures on TalkBack). They skip your navigation entirely and assume your site has no menu, then leave.&lt;/p&gt;

&lt;p&gt;Wrap the menu container in a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; element and add an &lt;code&gt;aria-label="Main"&lt;/code&gt; to distinguish it from any secondary navigation (footer menu, breadcrumbs). Most modern themes already do this, but custom-built sites and older themes often do not.&lt;/p&gt;

&lt;p&gt;The check is fast: open your site on your phone, open the browser's developer tools (Safari on Mac connected via USB, or Chrome remote debugging), and look at the menu container. If the outer element is &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;, you have work to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 4: The menu opens but does not move keyboard focus
&lt;/h2&gt;

&lt;p&gt;Tap the hamburger and the menu slides in from the side. Visually that works. Now imagine you reached the trigger by pressing Tab on a Bluetooth keyboard. After you press Enter to open the menu, where does your focus go?&lt;/p&gt;

&lt;p&gt;In most broken implementations, focus stays on the now-hidden trigger button. Pressing Tab takes you to whatever was after the trigger in the DOM (often a logo or a search field underneath the menu) rather than into the menu itself. Keyboard users are stuck navigating around their own open menu.&lt;/p&gt;

&lt;p&gt;The accessible behaviour is for focus to move to the first link inside the menu when it opens, and for Escape to close the menu and return focus to the trigger. This is harder to fix from a no-code interface because it requires JavaScript. On the major platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Squarespace, Wix, and Shopify default mobile menus generally manage focus correctly.&lt;/li&gt;
&lt;li&gt;Webflow custom interactions and WordPress custom menu plugins frequently do not.&lt;/li&gt;
&lt;li&gt;If you are on a custom-coded site, this is a 30-line JavaScript fix your developer can apply in less than an hour.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Issue 5: The close button is invisible to keyboard users
&lt;/h2&gt;

&lt;p&gt;Once the menu is open, the user needs a way to close it. Screen readers and keyboard users navigate by tab order, not by visual position, so a close button that is positioned absolutely at the top-right of the screen is not necessarily reachable in a sensible tab order.&lt;/p&gt;

&lt;p&gt;What works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Place the close button as the first focusable element inside the menu, so the moment focus enters the menu the close action is immediately available.&lt;/li&gt;
&lt;li&gt;Make sure Escape closes the menu in addition to the close button.&lt;/li&gt;
&lt;li&gt;Give the close button a descriptive label: &lt;code&gt;aria-label="Close menu"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common shortcut on platforms that do not let you reorder the close button is to make the trigger button itself toggle: when the menu is open, the same icon (often morphed into an X) closes it. Make sure the &lt;code&gt;aria-label&lt;/code&gt; updates with the state ("Open menu" when closed, "Close menu" when open).&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 6: Submenu chevrons are unreachable on touch and keyboard
&lt;/h2&gt;

&lt;p&gt;Many hamburger menus include nested categories that expand when tapped. The expand control is often a small chevron icon to the right of the parent label. Three failures stack here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The chevron is too small to tap reliably (less than the WCAG 2.5.5 recommended 44x44 pixel target). Mobile users with motor impairments, including older users with arthritis, cannot hit it.&lt;/li&gt;
&lt;li&gt;The chevron is rendered as an icon font with no accessible name. Screen readers announce "button" with no indication that it expands a submenu.&lt;/li&gt;
&lt;li&gt;The chevron is positioned in a way that conflicts with the link text, so users who want to expand the submenu accidentally activate the parent link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cleanest fix is to make the entire row tappable as a "expand submenu" action, with a separate, clearly labelled "Go to Section Name" link inside the expanded submenu. If your platform does not support that pattern, expand the chevron's hit area to at least 44x44 pixels via CSS and add &lt;code&gt;aria-label="Expand SectionName submenu"&lt;/code&gt; plus &lt;code&gt;aria-expanded&lt;/code&gt; to the chevron button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 7: The menu does not respect device settings
&lt;/h2&gt;

&lt;p&gt;The final issue is the easiest to overlook because it does not show up in axe or Lighthouse. Many hamburger menus animate in with a slide or scale effect, ignoring the user's &lt;code&gt;prefers-reduced-motion&lt;/code&gt; setting. For users with vestibular disorders, that quick slide can trigger nausea or migraine.&lt;/p&gt;

&lt;p&gt;If your platform offers an "animate menu open" toggle, set it to a static fade or no animation at all. If you must keep the slide animation for users without reduced-motion preferences, wrap the animation in a CSS media query so only those users see it. Most modern themes do this correctly; older custom themes and template-marketplace purchases often do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 10-minute self-check
&lt;/h2&gt;

&lt;p&gt;You do not need to read a 60-page WCAG document to identify whether your hamburger menu is in trouble. Try this on your own phone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable VoiceOver (iOS) or TalkBack (Android).&lt;/li&gt;
&lt;li&gt;Tap the hamburger trigger and listen. You should hear something like "Open menu, button". If you hear just "button", you have Issue 1.&lt;/li&gt;
&lt;li&gt;Open the menu. The screen reader should automatically read "Close menu" or jump to the first item. If it stays on the trigger, you have Issues 2 and 4.&lt;/li&gt;
&lt;li&gt;Try to swipe-navigate through the menu items. If the screen reader skips items or reads icons aloud as "image", you have Issue 6.&lt;/li&gt;
&lt;li&gt;Try to close the menu. If the only way out is to reload the page, you have Issue 5.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your menu fails on three or more of these checks, it almost certainly contributes to the 80% of failed Lighthouse audits that we see on small-business sites. The good news is that the fixes are concrete and platform-specific, and most of them can be applied from inside your website builder without writing a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/accessible-navigation-guide/"&gt;Accessible Navigation Guide&lt;/a&gt; -- the broader pattern beyond hamburger menus&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/sticky-header-accessibility/"&gt;Sticky Header Accessibility&lt;/a&gt; -- the other mobile-nav pattern that quietly traps users&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/five-minute-accessibility-audit/"&gt;Five-Minute Accessibility Audit&lt;/a&gt; -- a fast, free way to find the rest of the issues on your site&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;We're building a simple accessibility checker for non-developers -- no DevTools, no jargon. &lt;a href="https://dev.to/about"&gt;Join our waitlist&lt;/a&gt; to get early access.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PreToolUse Hooks Are a Tiny State Machine: The Four Exit Codes That Actually Matter</title>
      <dc:creator>AgentKit</dc:creator>
      <pubDate>Sat, 02 May 2026 05:07:28 +0000</pubDate>
      <link>https://dev.to/agentkit/pretooluse-hooks-are-a-tiny-state-machine-the-four-exit-codes-that-actually-matter-2bg3</link>
      <guid>https://dev.to/agentkit/pretooluse-hooks-are-a-tiny-state-machine-the-four-exit-codes-that-actually-matter-2bg3</guid>
      <description>&lt;p&gt;If you have read the Claude Code Hooks documentation, the example you remember is probably one that prints to stderr and exits 0. That example is correct, and it is where most tutorials stop. The four exit codes that decide whether your hook is safe in production are the ones the docs barely mention.&lt;/p&gt;

&lt;p&gt;We run a &lt;code&gt;PreToolUse&lt;/code&gt; hook on our own monorepo every working day, and the shape it settled into is not what we expected when we wrote the first version. What looked like a single conditional turned out to be a state machine with four branches. The hook grew into that shape because each of the four cases meant something different to a production team, and collapsing any two caused real bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a PreToolUse hook
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook is a script Claude Code runs immediately before a tool call — a Bash command, a file write, an MCP invocation. It receives the tool input as JSON on stdin and can do three things: pass the call through, modify the input, or signal deny or human confirmation. It communicates through two channels: an exit code, and an optional JSON object on stdout.&lt;/p&gt;

&lt;p&gt;Most introductory examples show the simplest case — a script that prints a log line, exits 0, lets the command run. That is fine for personal use. Once a second person uses your config, or once a single hook governs commands you actually care about — anything that touches production data, costs money, or is hard to undo — the simple case is not sufficient.&lt;/p&gt;

&lt;p&gt;The hook we run came out of a different need. We were paying a non-trivial token bill on certain CLI calls (&lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;), and wanted to silently rewrite them to use a proxy that returns trimmed output. The first version was a one-liner that always rewrote and always allowed. It broke within a day. Some commands had no equivalent rewrite. Others were dangerous enough that we did not want to silently approve them. By the time the hook stabilized, it had four branches.&lt;/p&gt;

&lt;p&gt;The snippets below are excerpted from &lt;code&gt;rtk-rewrite.sh&lt;/code&gt;, the open-source Claude Code hook shipped by rtk-ai/rtk under Apache 2.0. The full source is at &lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;github.com/rtk-ai/rtk&lt;/a&gt;. "rtk" is a trademark of that project; we use the name only to attribute the source. We picked it because it ships an explicit four-state exit-code protocol in its file header, which is rare in published hook code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expressive case — allow with rewrite (exit 0)
&lt;/h2&gt;

&lt;p&gt;The first branch does the most work and is the one most tutorials skip. The hook receives a command, decides how to run it, rewrites the input, and tells Claude Code to skip user confirmation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hookSpecificOutput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecisionReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RTK auto-rewrite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updatedInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rtk git status"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details inside this object look minor and turn out to be load-bearing. &lt;code&gt;permissionDecision: "allow"&lt;/code&gt; tells Claude Code to skip the user prompt entirely. It is the right default for a rewrite that is genuinely safe and transparent, and the fastest way to disable the permission model if used carelessly. We have not yet seen a hook that started with three &lt;code&gt;allow&lt;/code&gt; branches and ended up with fewer than five within a month.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;permissionDecisionReason&lt;/code&gt; ends up in your audit logs. It is the line your future self reads at 11pm on a Friday when something has gone wrong and you are trying to remember why a particular &lt;code&gt;Bash&lt;/code&gt; call was waved through three weeks ago. Leaving it empty is technically valid. It is also the audit-log equivalent of a commit with the message "stuff."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;updatedInput&lt;/code&gt; is the part most early hook designs do not use. The hook is not just deciding allow versus deny — it is rewriting the command into something different, then allowing the rewritten version. A raw &lt;code&gt;rm -rf&lt;/code&gt; can be silently translated into a &lt;code&gt;safer-rm&lt;/code&gt; wrapper before approval. The original command never runs.&lt;/p&gt;

&lt;p&gt;This branch deserves the name "expressive" because you can encode genuinely useful policy in it. Trim large outputs. Route a CLI call through a caching proxy. Replace a destructive command with a reversible one. The cost is the discipline of filling in &lt;code&gt;permissionDecisionReason&lt;/code&gt;. Skip that, and the branch becomes a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest case — passthrough when you have no opinion (exit 1)
&lt;/h2&gt;

&lt;p&gt;The second branch does nothing, and writing it correctly is harder than it sounds. Exit code 1 means the hook examined the command and has no opinion. Claude Code's native flow takes over, as if the hook had never been installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;0&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REWRITTEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;
  1&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# No equivalent rewrite — let it pass.&lt;/span&gt;
  ...
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;exit 0&lt;/code&gt; after the comment looks like a copy-paste error. It is not. The shell hook exits cleanly with no JSON on stdout — that is how you tell Claude Code "I have nothing to say." The internal exit-1 from the underlying binary is private signaling Claude Code never sees. Outside the hook, the only contract is the shell exit and the optional JSON.&lt;/p&gt;

&lt;p&gt;The alternative is a hook that claims jurisdiction over commands it does not understand. We have seen exactly one bug class in this layer, always the same shape: a hook accidentally treated an unknown command as if it had a rewrite, returned a malformed &lt;code&gt;updatedInput&lt;/code&gt;, and Claude Code dutifully ran a garbage command. The fallout was twelve minutes of &lt;code&gt;Bash&lt;/code&gt; calls returning empty strings before we noticed. When in doubt, exit zero with no JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  The handoff case — let Claude Code's own deny rule decide (exit 2)
&lt;/h2&gt;

&lt;p&gt;The third branch is where production teams tend to over-engineer. When a hook detects something dangerous, the temptation is to deny it directly. The hookSpecificOutput object will let you do exactly that. We do not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;2&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# Deny rule matched — let Claude Code's native deny rule handle it.&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The branch detects that a command matches a deny rule, then exits cleanly, prints no JSON, and lets the call continue past the hook. The actual deny is handled downstream by Claude Code's &lt;code&gt;permissions.deny&lt;/code&gt; configuration, which has its own UI, audit format, and override mechanism.&lt;/p&gt;

&lt;p&gt;Two reasons we route this way. Deny rules in &lt;code&gt;permissions.deny&lt;/code&gt; are configured in the same place as the rest of the team's permission policy — a teammate can see, in one file, what is denied. A regex buried in a Bash conditional is not reviewable. It decays into folklore within two months.&lt;/p&gt;

&lt;p&gt;The override path also matters. When Claude Code denies via its native rule, the user can negotiate — re-prompt, request an exception, escalate. A hook that denies directly via &lt;code&gt;permissionDecision: "deny"&lt;/code&gt; makes the path back more awkward and lands the audit entry under a different category. So the hook signals "deny territory" with internal exit code 2, then steps back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting case — rewrite, then hand the question to a human (exit 3)
&lt;/h2&gt;

&lt;p&gt;The fourth branch is the one we use most often, and the one we did not realize we needed until the hook had been running for two weeks. It rewrites the command the way the first branch does, but omits &lt;code&gt;permissionDecision&lt;/code&gt;. Claude Code sees the rewritten input, sees no decision, and prompts the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;3&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# Ask: rewrite the command, omit permissionDecision so Claude Code prompts.&lt;/span&gt;
  jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--argjson&lt;/span&gt; updated &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UPDATED_INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'{
      "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "updatedInput": $updated
      }
    }'&lt;/span&gt;
  &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single absence — no &lt;code&gt;permissionDecision&lt;/code&gt; field — is the entire mechanism. The hook is saying "I have shaped the command, but I am not approving it." Claude Code interprets the missing field as a request for human confirmation, shows the user the rewritten command, and waits.&lt;/p&gt;

&lt;p&gt;The interesting commands in our monorepo are not the ones we want to kill, and not the ones we want to auto-allow. They are the ones we want a teammate to look at for two seconds before letting through. A migration that touches production. A delete against a shared bucket. A force-push to a branch that is sometimes deployed. The hook does the boring work — substitute the right wrapper, add the right flags, point at the right environment — then hands a clean command back to a human for the final yes.&lt;/p&gt;

&lt;p&gt;The first branch is for rewrites so safe that asking would be insulting. The fourth is for rewrites that are helpful but not safe enough to auto-approve. Production hooks live in the fourth branch more than the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surrounds the four states
&lt;/h2&gt;

&lt;p&gt;The four-state machine is the core, but the hook in production is held together by three smaller habits. They sound like operational hygiene; they are what keeps the state machine from failing quietly.&lt;/p&gt;

&lt;p&gt;The kill switch came first. It is four lines at the top of every agent script we ship, and predates the hook itself. It checks for a sentinel file at a known path, and exits cleanly if it exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/agentkit-ccpack-pause &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[kill-switch] Paused. Exiting."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added this after a separate side project of ours ran six days of unsupervised automation — technically correct on every individual loop, catastrophically wrong as a whole. What was missing was a way to pull the plug from outside. A four-line check is the difference between a six-day incident and a thirty-second one — any agent on the machine can be stopped by one &lt;code&gt;touch&lt;/code&gt;, no code changes required.&lt;/p&gt;

&lt;p&gt;The version guard followed within a week. The hook depends on a particular version of an external binary, and that binary has shipped breaking changes. The guard parses the version string and exits cleanly with a stderr warning if it is too old.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RTK_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;rtk &lt;span class="nt"&gt;--version&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]+\.[0-9]+\.[0-9]+'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RTK_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RTK_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;MINOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RTK_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAJOR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MINOR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 23 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[rtk] WARNING: rtk &lt;/span&gt;&lt;span class="nv"&gt;$RTK_VERSION&lt;/span&gt;&lt;span class="s2"&gt; is too old (need &amp;gt;= 0.23.0)."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, an old binary keeps the hook running with the wrong semantics. Deny rules might be evaluated against the new command shape; rewrites might target obsolete flags. The hook does not crash — it just gets quietly wrong. The guard is the hook's way of refusing to run when the ground has shifted under it.&lt;/p&gt;

&lt;p&gt;The dependency guard was the last to land, after we lost an evening to a missing &lt;code&gt;jq&lt;/code&gt;. The hook checks that its required tools are on the path, and warns to stderr and exits cleanly if any are missing. Not exit 1, which would propagate as a hook failure and freeze the session. Exit 0, with a visible warning, so the session stays alive while the hook becomes a no-op.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; jq &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[rtk] WARNING: jq is not installed. Hook cannot rewrite commands."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silent failure is the largest single bug source we have seen in production hooks. A graceful no-op is louder than a crash — the warning shows up every run — but less destructive than a frozen &lt;code&gt;Bash&lt;/code&gt; call. The pattern across all three guards is the same: fail open with a visible warning, never fail closed in silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the four states together
&lt;/h2&gt;

&lt;p&gt;The hook is not a script that prints something. It is a state machine with four exits, and a wrapper of operational guards around them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command in
     │
     ▼
 [hook logic]
     │
     ├─ exit 0 + JSON ────▶ allow + (optional rewrite)
     ├─ exit 1 ───────────▶ passthrough (no JSON)
     ├─ exit 2 ───────────▶ deny-handoff (let Claude Code's deny rule act)
     └─ exit 3 + JSON ────▶ ask (rewrite, but human confirms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the four states are visible, the implementation almost asks for a particular shape. Judgment — which state are we in for this command — wants to live somewhere testable. Response shaping — printing the right JSON, choosing the right exit code — wants to live in the shell. The rtk hook puts judgment in a Rust binary and lets the shell do I/O. The shell hook is the postman. The binary is the brain. Not every team needs a Rust binary, but the separation between "decide" and "respond" is what lets you write tests against decision logic at all. Decisions written in Bash conditionals are decisions you cannot verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves a production team
&lt;/h2&gt;

&lt;p&gt;Once you see the four branches, you stop writing hooks that pretend to be opinions and start writing hooks that are postmen. Allow with a real reason. Pass through honestly when you have no opinion. Hand denies back to the platform's own rule. Rewrite-then-ask for the cases where a human still needs the final word.&lt;/p&gt;

&lt;p&gt;Claude Code Hooks are a fine extension layer. Not every team needs a Rust binary, and not every config needs all four branches on day one. But the four-state shape is universal, and the day you wish you had separated judgment from I/O is the day someone runs &lt;code&gt;rm -rf&lt;/code&gt; against staging.&lt;/p&gt;

&lt;p&gt;Not legal advice. The patterns here come from our own production usage; your config will need review by someone with context on your team's actual risks.&lt;/p&gt;




&lt;p&gt;We are open-sourcing the AgentKit Hooks Pack — production-ready templates for &lt;code&gt;PreToolUse&lt;/code&gt; permission gating, &lt;code&gt;PostToolUse&lt;/code&gt; audit logs, kill switch sentinels, notification routing — under Apache 2.0 in late May. The day it lands, we email a launch note plus the Companion Guide PDF (sixty pages on lifecycle events, failure modes, and rollout patterns) to the pre-launch list. To get on it: &lt;a href="https://imta71770-dot.github.io/agentkit-hooks-pack/" rel="noopener noreferrer"&gt;imta71770-dot.github.io/agentkit-hooks-pack&lt;/a&gt;. The repo itself, with the README and license, lives at &lt;a href="https://github.com/imta71770-dot/agentkit-hooks-pack" rel="noopener noreferrer"&gt;github.com/imta71770-dot/agentkit-hooks-pack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Code snippets in this article are excerpted from &lt;code&gt;rtk-rewrite.sh&lt;/code&gt; in &lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;rtk-ai/rtk&lt;/a&gt;, licensed under Apache 2.0. "rtk" is a trademark of that project and is used here for attribution only.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>devtools</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
