{"id":"l-a11y-4-2","courseId":"web-accessibility","lang":"en","kind":"lesson","summary":"Keep Ticketly usable at 400% zoom and with custom text spacing, size touch targets properly, and respect users who need less motion with prefers-reduced-motion and pause controls.","html":"<p>Maha reads Ticketly at 400% zoom on a 13-inch laptop. At that zoom, her browser window is effectively 320 CSS pixels wide. The festival cards, laid out in three fixed 320px columns, force her to scroll sideways on every line of text, which at that magnification means losing her place constantly. Meanwhile the homepage marquee scrolls artist names forever, which gives her colleague Tom, who has a vestibular disorder, a headache within a minute.</p>\n<p>This lesson covers what happens when people change the size of your page, and when your page moves on its own.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Zoom and reflow</h2>\n<p>Three criteria work together:</p>\n<ul>\n<li><strong>1.4.4 Resize Text (AA):</strong> text can be zoomed to 200% without losing content or function.</li>\n<li><strong>1.4.10 Reflow (AA):</strong> at a width of <strong>320 CSS pixels</strong>, content fits without horizontal scrolling. (A 1280px window at 400% zoom is 320 CSS pixels wide.) Data tables, maps and diagrams are exempt, because they need two dimensions.</li>\n<li><strong>1.4.12 Text Spacing (AA):</strong> nothing breaks when users override line height to 1.5, paragraph spacing to 2em, letter spacing to 0.12em and word spacing to 0.16em, as dyslexic readers often do with a browser extension.</li>\n</ul>\n<p>If you build responsively, you are most of the way there. The common culprits are fixed widths and fixed heights. Ticketly’s card grid:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">before.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.cards</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">grid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  grid-template-columns</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">repeat</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">320</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  gap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.card</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;      </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* clips the title when text spacing grows */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  font-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* doesn't follow the user's default font size */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">after.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.cards</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">grid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  grid-template-columns</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">repeat</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto-fit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">minmax</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">min</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">fr</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  gap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.card</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  font-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.125</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The <code dir=\"ltr\">min(100%, 16rem)</code> part matters: on a 320px viewport a 16rem column (256px plus padding) could still overflow, and <code dir=\"ltr\">min()</code> lets the column shrink to the container. Sizing text in <code dir=\"ltr\">rem</code> means it scales when a user sets a larger default font size in their browser, which zoom alone doesn’t cover.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Disabling pinch zoom</span></p>\n<p><code dir=\"ltr\">&lt;meta name=&quot;viewport&quot; content=&quot;…, maximum-scale=1, user-scalable=no&quot;&gt;</code> was copied into countless sites to stop “accidental zooming”. It stops deliberate zooming too, which is how people with low vision use a phone. Use <code dir=\"ltr\">width=device-width, initial-scale=1</code> and nothing more.</p>\n</aside>\n<p>To test reflow, set your browser window to 1280px wide and zoom to 400%, or use responsive mode at 320px. Scroll vertically through the whole page. Any horizontal scrollbar, overlapping text or cut-off button is a bug.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Target size, revisited</h2>\n<p>You met 2.5.8 Target Size (Minimum) in <a href=\"/lesson/web-accessibility/l-a11y-1-3\">WCAG 2.2, POUR and the Law</a>: pointer targets of at least 24 by 24 CSS pixels, or enough spacing around smaller ones. Ticketly’s quantity stepper used 16px icon buttons touching each other. The fix is padding, not bigger icons:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.stepper</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  min-inline-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* 44px, comfortably above the 24px minimum */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  min-block-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Aim higher than the minimum for primary actions on touch screens. Apple’s and Google’s platform guidelines suggest around 44 to 48 pixels, and people with tremor will thank you.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Motion: pause it, and respect the preference</h2>\n<p>Movement causes two different problems. Content that moves on its own competes for attention, which is a real barrier for people with attention disorders and anyone reading slowly. And large motion, such as parallax, zooming transitions or sliding panels, can trigger dizziness and nausea in people with vestibular disorders.</p>\n<p>WCAG addresses the first with <strong>2.2.2 Pause, Stop, Hide (A)</strong>: anything that starts automatically, moves (or blinks or scrolls) for more than five seconds and appears alongside other content needs a way to pause, stop or hide it. <strong>2.3.1 Three Flashes or Below Threshold (A)</strong> forbids content that flashes more than three times in any one second, unless the flashes are small and dim enough to stay under the flash thresholds, because flashing can trigger seizures.</p>\n<p>For the second, operating systems let users ask for reduced motion (on macOS: Accessibility, Display, Reduce motion; on Windows: Accessibility, Visual effects, Animation effects off), and CSS can read that setting:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">ticketly/motion.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: scroll-names </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> infinite</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.marquee--paused</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> .marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-play-state</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">paused</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: reduce) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: opacity </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* a fade instead of a slide */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"marquee-toggle\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Pause animation&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>The pause button is still needed: many people who are bothered by motion never find the system setting, and 2.2.2 requires a control on the page. “Reduced” doesn’t have to mean “none”. A short fade or colour change rarely causes trouble; it is large movement across the screen that does.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 190\" role=\"img\" aria-labelledby=\"l-a11y-4-2-t1\"><defs><marker id=\"arrow-l-a11y-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-a11y-4-2-t1\">The operating system reduce motion setting flows through the prefers-reduced-motion media query to remove large animations by default, while an on-page pause button lets anyone stop the marquee; together they cover users who set the preference and users who don't.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"30\" width=\"190\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"115\" y=\"63\" text-anchor=\"middle\">OS: Reduce motion</text>\n  <path class=\"d-arrow\" d=\"M210 58 L270 58\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"275\" y=\"30\" width=\"200\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"375\" y=\"63\" text-anchor=\"middle\">prefers-reduced-motion</text>\n  <rect class=\"d-box\" x=\"20\" y=\"110\" width=\"190\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"115\" y=\"143\" text-anchor=\"middle\">Pause button</text>\n  <path class=\"d-arrow\" d=\"M210 138 L270 138\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"275\" y=\"110\" width=\"200\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"375\" y=\"143\" text-anchor=\"middle\">animation-play-state</text>\n  <path class=\"d-arrow\" d=\"M475 58 L530 95\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M475 138 L530 105\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-success\" x=\"535\" y=\"72\" width=\"130\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"600\" y=\"105\" text-anchor=\"middle\">Calm page</text>\n</svg><figcaption>Two layers of motion control</figcaption></figure>\n<p>JavaScript-driven animations need the same check. <code dir=\"ltr\">window.matchMedia('(prefers-reduced-motion: reduce)').matches</code> tells you the current preference, and you can listen for its <code dir=\"ltr\">change</code> event to react if the user changes it while the page is open.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Video and auto-playing media</span></p>\n<p>Background videos count as moving content, and any audio that plays automatically for more than three seconds needs a pause or volume control (1.4.2 Audio Control). The simplest compliant design is not to autoplay at all.</p>\n</aside>\n<p>Ticketly now holds together when people zoom it, space its text, tap it with a shaky hand or ask it to keep still. The next lesson returns to the accessibility tree and looks closely at how every control gets the name a screen reader speaks.</p>\n","toc":[{"id":"sec-1","text":"Zoom and reflow","level":2},{"id":"sec-2","text":"Target size, revisited","level":2},{"id":"sec-3","text":"Motion: pause it, and respect the preference","level":2}],"takeaways":["Content must reflow into a 320 CSS pixel wide viewport, the width of a 1280px window at 400% zoom, without horizontal scrolling.","Size text in rem, avoid fixed heights on text containers, and never disable pinch zoom in the viewport meta tag.","Anything that moves on its own for more than five seconds needs a way to pause, stop or hide it (2.2.2).","Wrap non-essential animation in a prefers-reduced-motion check, replacing big movement with fades or nothing."],"further":[{"title":"W3C: Understanding 1.4.10 Reflow","url":"https://www.w3.org/WAI/WCAG22/Understanding/reflow.html"},{"title":"MDN: prefers-reduced-motion","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion"},{"title":"W3C: Understanding 2.2.2 Pause, Stop, Hide","url":"https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html"},{"title":"W3C: Understanding 1.4.12 Text Spacing","url":"https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html"}],"quiz":[{"q":"<p>Why does 1.4.10 Reflow test at a width of 320 CSS pixels?</p>","options":[{"text":"It is the width of the smallest phone still sold","why":"Phones are a side benefit. The number comes from desktop zoom, not from a device list."},{"text":"A 1280px wide browser window zoomed to 400% has a viewport of 320 CSS pixels","why":"Correct. Someone with low vision at 400% zoom on a laptop gets the same layout as a narrow phone, so a responsive layout serves both."},{"text":"It is the default minimum width in CSS grid","why":"CSS grid has no such default. The value comes from the zoom calculation in the Understanding document."}],"answer":1},{"q":"<p>Which viewport meta tag fails WCAG 1.4.4 Resize Text?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">meta</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"viewport\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> content</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"It passes, because it sets the initial scale correctly","why":"The first two values are fine. The last two stop people from pinch-zooming on mobile, which is exactly what low-vision users rely on."},{"text":"It fails only on desktop browsers","why":"Desktop browsers ignore the viewport meta tag. It is mobile users who lose the ability to zoom."},{"text":"It fails, because maximum-scale=1 and user-scalable=no block pinch zoom","why":"Correct. Remove both. Some mobile browsers override them for accessibility, but you can’t rely on that."}],"answer":2},{"q":"<p>Ticketly’s hero banner auto-scrolls artist names in a marquee forever. What does WCAG 2.2 require at Level A?</p>","options":[{"text":"A visible control to pause, stop or hide it, because it moves for more than five seconds","why":"Correct. 2.2.2 Pause, Stop, Hide applies to moving content that starts automatically, lasts more than five seconds and sits beside other content."},{"text":"Nothing, as long as it stops when prefers-reduced-motion is set","why":"Honouring the preference is good practice, but many people who are distracted by motion never set it. 2.2.2 requires a control on the page."},{"text":"An aria-live region so screen readers announce each new name","why":"That would flood screen reader users with announcements. The issue is the motion itself, not missing announcements."}],"answer":0},{"q":"<p>Which CSS respects a user’s reduced-motion setting for Ticketly’s sliding cards?</p>","options":[{"text":"<code dir=\"ltr\">@media (prefers-reduced-motion: no-preference) { .card { transition: none; } }</code>","why":"This removes the animation for the users who didn’t ask for less motion, and keeps it for those who did. The condition is backwards."},{"text":"<code dir=\"ltr\">.card { transition: transform 0.4s; } @media (prefers-reduced-motion: reduce) { .card { transition: opacity 0.2s; } }</code>","why":"Correct. The default keeps the slide, and the media query swaps it for a short fade when the user has asked for reduced motion."},{"text":"<code dir=\"ltr\">@media (motion: reduce) { .card { transition: none; } }</code>","why":"There is no motion media feature. The feature is prefers-reduced-motion."},{"text":"<code dir=\"ltr\">.card[aria-reduced-motion] { transition: none; }</code>","why":"No such ARIA attribute exists. Users set the preference in their operating system, and CSS reads it with prefers-reduced-motion."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"marquee\" aria-label=\"Artists\">\n  <div class=\"marquee__track\">Nour Quartet · The Saltmarsh Band · Layla &amp; the Tides · Harbour Brass · DJ Marwa</div>\n</section>\n\n<div class=\"cards\">\n  <article class=\"card\">\n    <h3>Friday pass</h3>\n    <p>SAR 180</p>\n    <div class=\"stepper\">\n      <button type=\"button\" aria-label=\"Fewer Friday passes\">−</button>\n      <span>1</span>\n      <button type=\"button\" aria-label=\"More Friday passes\">+</button>\n    </div>\n  </article>\n  <article class=\"card\"><h3>Saturday pass</h3><p>SAR 180</p></article>\n  <article class=\"card\"><h3>Weekend pass</h3><p>SAR 320</p></article>\n</div>","css":".marquee { overflow: hidden; background: #f1eefb; padding: 8px 0; margin-block-end: 16px; }\n.marquee__track { white-space: nowrap; animation: scroll-names 12s linear infinite; }\n@keyframes scroll-names { from { transform: translateX(100%); } to { transform: translateX(-100%); } }\n\n.cards { display: grid; grid-template-columns: repeat(3, 320px); gap: 16px; }\n.card { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; }\n.card h3 { margin: 0; font-size: 18px; }\n.stepper { display: flex; align-items: center; gap: 0; }\n.stepper button { width: 16px; height: 16px; padding: 0; font-size: 12px; line-height: 1; border: 1px solid #5b3cc4; background: #fff; }\n.stepper span { padding-inline: 8px; }\n","js":""},"solution":{"html":"<section class=\"marquee\" aria-label=\"Artists\">\n  <button type=\"button\" class=\"marquee-toggle\" aria-pressed=\"false\">Pause animation</button>\n  <div class=\"marquee__track\">Nour Quartet · The Saltmarsh Band · Layla &amp; the Tides · Harbour Brass · DJ Marwa</div>\n</section>\n\n<div class=\"cards\">\n  <article class=\"card\">\n    <h3>Friday pass</h3>\n    <p>SAR 180</p>\n    <div class=\"stepper\">\n      <button type=\"button\" aria-label=\"Fewer Friday passes\">−</button>\n      <span>1</span>\n      <button type=\"button\" aria-label=\"More Friday passes\">+</button>\n    </div>\n  </article>\n  <article class=\"card\"><h3>Saturday pass</h3><p>SAR 180</p></article>\n  <article class=\"card\"><h3>Weekend pass</h3><p>SAR 320</p></article>\n</div>","css":".marquee { overflow: hidden; background: #f1eefb; padding: 8px 0; margin-block-end: 16px; }\n.marquee__track { white-space: nowrap; animation: scroll-names 12s linear infinite; }\n.marquee--paused .marquee__track { animation-play-state: paused; }\n@keyframes scroll-names { from { transform: translateX(100%); } to { transform: translateX(-100%); } }\n\n@media (prefers-reduced-motion: reduce) {\n  .marquee__track { animation: none; }\n}\n\n.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1rem; }\n.card { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; }\n.card h3 { margin: 0; font-size: 1.125rem; }\n.stepper { display: flex; align-items: center; gap: 4px; }\n.stepper button { min-inline-size: 2.75rem; min-block-size: 2.75rem; padding: 0; font-size: 1.25rem; line-height: 1; border: 1px solid #5b3cc4; background: #fff; }\n.stepper span { padding-inline: 8px; }\n","js":"const marquee = document.querySelector('.marquee');\nconst toggle = document.querySelector('.marquee-toggle');\n\ntoggle.addEventListener('click', () => {\n  const paused = toggle.getAttribute('aria-pressed') === 'true';\n  toggle.setAttribute('aria-pressed', String(!paused));\n  marquee.classList.toggle('marquee--paused', !paused);\n});\n"},"checks":[{"id":"reduced-motion","test":"const track = $('.marquee__track'); const rules = []; const walk = (list) => { for (const r of list) { if (r.media && r.conditionText && /prefers-reduced-motion\\s*:\\s*reduce/.test(r.conditionText)) rules.push(...r.cssRules); else if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); return rules.some((r) => r.selectorText && track.matches(r.selectorText) && (r.style.animationName === 'none' || /(^|\\s)none(\\s|$)/.test(r.style.animation) || r.style.animationPlayState === 'paused' || /^0(s|ms)?$/.test(r.style.animationDuration) || r.style.animationIterationCount === '1'));"},{"id":"pause-control","test":"const btn = $$('button').find((b) => /pause|stop/i.test(b.textContent + ' ' + (b.getAttribute('aria-label') || ''))); if (!btn) return false; const track = $('.marquee__track'); if (getComputedStyle(track).animationPlayState === 'paused') return false; await click(btn); const cs = getComputedStyle(track); return cs.animationPlayState === 'paused' || cs.animationName === 'none';"},{"id":"reflow","test":"const doc = document.documentElement; const cards = $('.cards'); return doc.scrollWidth <= doc.clientWidth && cards.scrollWidth <= cards.clientWidth + 1;"},{"id":"target-size","test":"const bs = $$('.stepper button'); return bs.length === 2 && bs.every((b) => { const r = b.getBoundingClientRect(); return r.width >= 24 && r.height >= 24; });"}],"title":"Calm down and loosen up the homepage","prompt":"<p>Ticketly’s homepage scrolls artist names forever, forces a three-column grid of fixed 320px cards, and uses 16px stepper buttons. Fix it:</p>\n<ol>\n<li>Add a <code dir=\"ltr\">@media (prefers-reduced-motion: reduce)</code> block that turns off the <code dir=\"ltr\">.marquee__track</code> animation.</li>\n<li>Add a <strong>button</strong> whose text contains “Pause” that pauses the marquee when clicked (for example with <code dir=\"ltr\">animation-play-state: paused</code>).</li>\n<li>Make the <code dir=\"ltr\">.cards</code> grid reflow so nothing scrolls horizontally in a narrow viewport (the preview is only about 300px wide).</li>\n<li>Make both <code dir=\"ltr\">.stepper</code> buttons at least 24 × 24 CSS pixels.</li>\n</ol>\n","hints":["Inside the media query: .marquee__track { animation: none; }","Toggle a class such as .marquee–paused on the section from a button click, and set animation-play-state: paused on the track inside it. aria-pressed makes it a proper toggle.","grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)) lets the grid drop to one column. For the stepper, min-inline-size and min-block-size of 2.75rem give a comfortable 44px."],"explanation":"<p>The media query removes the marquee’s movement for everyone who has asked their operating system for reduced motion, and the pause button covers everyone else, which is what 2.2.2 Pause, Stop, Hide requires. <code dir=\"ltr\">auto-fit</code> with <code dir=\"ltr\">minmax(min(100%, 16rem), 1fr)</code> lets the grid fall back to a single column, so a 400% zoom (or a phone) never needs horizontal scrolling. The stepper buttons grow to 44px, well above 2.5.8’s 24px minimum, without making the icons themselves bigger.</p>\n","checkLabels":{"reduced-motion":"A prefers-reduced-motion: reduce block stops the marquee animation","pause-control":"A Pause button pauses the marquee","reflow":"The page and the card grid don’t scroll horizontally","target-size":"Both stepper buttons are at least 24 × 24 pixels"}},"words":767,"readMinutes":4}