Accessibility pattern · Layout and structure

Reflow

At 400% zoom a 1280-pixel window is 320 CSS pixels wide, so a page that reflows to 320 pixels is a page people can zoom: one column, read top to bottom, never scrolled sideways. Nothing on the page has a fixed width: grids collapse with container queries, rows wrap, long links break, and the readout measures the result instead of trusting it.

WCAG criteria
6
Keyboard rules
6
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

1280 pixels 100% zoom

A 1280-pixel window at 400% zoom leaves 320 CSS pixels for the page.

Zoom

No horizontal scrolling

Content width
1280 px
Preview scale
100%

Order 4417853 · placed 2 October

Brass table lamp and 2 more items

Out for delivery

Items
3
Total paid
₹4,860
Paid with
UPI
Arriving
Today, 2 to 5 pm

Where it is now

  1. Packed 2 Oct
  2. Shipped 3 Oct
  3. Out for delivery Today
  4. Delivered By 5 pm

Your parcel left the Bengaluru hub at 6:40 am and is with a courier in Indiranagar.

Follow it live: track.parcel.test/consignment/IN-BLR-2026-0004417853?ref=order-email

Updates go to

Delivery instructions

For example, ring twice or call when you arrive.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Arrow Left or Arrow RightOn the slider, narrows or widens the preview by 10 pixels; Arrow Down and Arrow Up do the same.
Page Down or Page UpNarrows or widens it by 160 pixels, the same in every browser.
Home or EndSets 320 pixels (400% zoom) or 1280 pixels (100% zoom).
Enter or SpaceOn a zoom button, sets the width that zoom level leaves: 1280, 640 or 320 pixels.
TabMoves on to the preview, which takes focus only while it scrolls, then through the page's own controls.
Arrow Down or Arrow UpIn the focused preview, scrolls it; in the fixed-width version, Arrow Left and Arrow Right scroll it sideways too.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the sliderViewport width, slider, 1280 pixels, 100% zoom. A 1280-pixel window at 400% zoom leaves 320 CSS pixels for the page.
Home320 pixels, 400% zoom
Focus reaches the 200% buttonZoom, group. 200%, toggle button, not pressed
In the fixed-width version, the width drops below 960 pixelsNeeds horizontal scrolling
Tab reaches the preview while it scrollsDemo page preview, region
Copy tracking link is pressedTracking link copied

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-reflow" data-ap-reflow>
  <div class="ap-reflow__bench">
    <div class="ap-reflow__top">
      <label class="ap-reflow__label" for="reflow-width">Viewport width</label>
      <output class="ap-reflow__now" for="reflow-width" aria-live="off"><span class="ap-reflow__num" data-ap-reflow-vw>1280</span> <span data-ap-reflow-unit>pixels</span> <span class="ap-reflow__sep" aria-hidden="true">·</span> <span class="ap-reflow__num"><span data-ap-reflow-zoom>100</span>%</span> <span data-ap-reflow-zword>zoom</span></output>
    </div>
    <p class="ap-hint" id="reflow-hint">A 1280-pixel window at 400% zoom leaves 320 CSS pixels for the page.</p>
    <input class="ap-reflow__range" id="reflow-width" type="range" min="320" max="1280" step="10" value="1280" aria-valuetext="1280 pixels, 100% zoom" aria-describedby="reflow-hint" data-ap-reflow-range />
    <div class="ap-reflow__ticks" aria-hidden="true">
      <span>320</span><span>640</span><span>960</span><span>1280</span>
    </div>
    <div class="ap-reflow__zoom" role="group" aria-labelledby="reflow-zoom-name">
      <span class="ap-reflow__zoom-name" id="reflow-zoom-name">Zoom</span>
      <div class="ap-reflow__seg">
        <button type="button" class="ap-reflow__preset" aria-pressed="true" data-ap-reflow-to="1280">100%</button>
        <button type="button" class="ap-reflow__preset" aria-pressed="false" data-ap-reflow-to="640">200%</button>
        <button type="button" class="ap-reflow__preset" aria-pressed="false" data-ap-reflow-to="320">400%</button>
      </div>
    </div>
  </div>

  <div class="ap-reflow__window">
    <div class="ap-reflow__chrome">
      <p class="ap-reflow__verdict" role="status" data-ap-reflow-verdict data-fits="true"><svg class="ap-reflow__icon ap-reflow__icon--fits" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8.2 12.4 2.6 2.6 5-5.2"/></svg><svg class="ap-reflow__icon ap-reflow__icon--scrolls" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 12h17"/><path d="m7.5 8-4 4 4 4"/><path d="m16.5 8 4 4-4 4"/></svg><span data-ap-reflow-verdict-text>No horizontal scrolling</span></p>
      <dl class="ap-reflow__measure">
        <div><dt>Content width</dt><dd><span data-ap-reflow-content>1280</span> <span translate="no">px</span></dd></div>
        <div><dt>Preview scale</dt><dd><span data-ap-reflow-scale>100</span>%</dd></div>
      </dl>
    </div>
    <div class="ap-reflow__screen" role="region" aria-label="Demo page preview" data-ap-reflow-screen>
      <div class="ap-reflow__sizer" data-ap-reflow-sizer>
        <div class="ap-reflow__frame" data-ap-reflow-frame>
          <div class="ap-reflow__page">
            <div class="ap-reflow__body">
              <div class="ap-reflow__head">
                <div class="ap-reflow__id">
                  <p class="ap-reflow__kicker">Order 4417853 · placed 2 October</p>
                  <h3 class="ap-reflow__name">Brass table lamp and 2 more items</h3>
                </div>
                <p class="ap-reflow__badge"><svg class="ap-reflow__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2.5 6.5h11v9h-11z"/><path d="M13.5 9.5h4l3 3.2v2.8h-7"/><circle cx="6.5" cy="17.5" r="1.8"/><circle cx="16.5" cy="17.5" r="1.8"/></svg><span>Out for delivery</span></p>
              </div>

              <div class="ap-reflow__actions" role="group" aria-label="Order actions">
                <button type="button" class="ap-btn" data-ap-reflow-copy><svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Copy tracking link</button>
                <button type="button" class="ap-btn" data-ap-reflow-edit><svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>Edit instructions</button>
                <button type="button" class="ap-btn" aria-expanded="false" aria-controls="reflow-items" data-ap-reflow-items><svg class="ap-btn__icon ap-reflow__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>Order items</button>
                <p class="ap-reflow__msg" role="status" data-ap-reflow-msg></p>
              </div>

              <ul class="ap-reflow__items" id="reflow-items" hidden>
                <li><span class="ap-reflow__item">Brass table lamp, 40 cm</span><span class="ap-reflow__qty">Qty 1</span><span class="ap-reflow__price">₹3,250</span></li>
                <li><span class="ap-reflow__item">Handloom cotton cushion covers, set of 2</span><span class="ap-reflow__qty">Qty 1</span><span class="ap-reflow__price">₹890</span></li>
                <li><span class="ap-reflow__item">Warm white LED bulb, 9 W</span><span class="ap-reflow__qty">Qty 2</span><span class="ap-reflow__price">₹720</span></li>
              </ul>

              <dl class="ap-reflow__stats">
                <div><dt>Items</dt><dd>3</dd></div>
                <div><dt>Total paid</dt><dd>₹4,860</dd></div>
                <div><dt>Paid with</dt><dd>UPI</dd></div>
                <div><dt>Arriving</dt><dd>Today, 2 to 5 pm</dd></div>
              </dl>

              <div class="ap-reflow__cols">
                <div class="ap-reflow__card">
                  <h4 class="ap-reflow__sub">Where it is now</h4>
                  <ol class="ap-reflow__journey">
                    <li class="ap-reflow__stop is-done">
                      <span class="ap-reflow__pin" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6.5 12.5 3.5 3.5 7.5-8"/></svg></span>
                      <span class="ap-reflow__what">Packed</span>
                      <span class="ap-reflow__when">2 Oct</span>
                    </li>
                    <li class="ap-reflow__stop is-done">
                      <span class="ap-reflow__pin" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6.5 12.5 3.5 3.5 7.5-8"/></svg></span>
                      <span class="ap-reflow__what">Shipped</span>
                      <span class="ap-reflow__when">3 Oct</span>
                    </li>
                    <li class="ap-reflow__stop" aria-current="step">
                      <span class="ap-reflow__pin" aria-hidden="true"></span>
                      <span class="ap-reflow__what">Out for delivery</span>
                      <span class="ap-reflow__when">Today</span>
                    </li>
                    <li class="ap-reflow__stop">
                      <span class="ap-reflow__pin" aria-hidden="true"></span>
                      <span class="ap-reflow__what">Delivered</span>
                      <span class="ap-reflow__when">By 5 pm</span>
                    </li>
                  </ol>
                  <p>Your parcel left the Bengaluru hub at 6:40 am and is with a courier in Indiranagar.</p>
                  <p><span>Follow it live:</span> <a class="ap-reflow__link" href="#reflow-track" translate="no" data-ap-reflow-link>track.parcel.test/consignment/IN-BLR-2026-0004417853?ref=order-email</a></p>
                  <p><span>Updates go to</span> <span class="ap-reflow__email" translate="no">[email protected]</span></p>
                </div>

                <form class="ap-reflow__form" data-ap-reflow-form>
                  <h4 class="ap-reflow__sub">Delivery instructions</h4>
                  <div class="ap-reflow__fields">
                    <div class="ap-field">
                      <label class="ap-label" for="reflow-leave">Leave it with</label>
                      <div class="ap-reflow__select">
                        <select class="ap-input" id="reflow-leave" name="leave" data-ap-reflow-first>
                          <option>Me only</option>
                          <option>Next door</option>
                          <option>The security desk</option>
                        </select>
                        <svg class="ap-reflow__caret" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
                      </div>
                    </div>
                    <div class="ap-field">
                      <label class="ap-label" for="reflow-note">Note for the courier</label>
                      <input class="ap-input" id="reflow-note" name="note" type="text" autocomplete="off" aria-describedby="reflow-note-hint" />
                      <p class="ap-hint" id="reflow-note-hint">For example, ring twice or call when you arrive.</p>
                    </div>
                  </div>
                  <div class="ap-reflow__submit">
                    <button type="submit" class="ap-btn ap-btn--primary">Save instructions</button>
                    <p class="ap-reflow__saved" role="status" data-ap-reflow-saved></p>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.4.10 Reflow Level AA

    At 320 CSS pixels the page is one column with nothing cut off and no sideways scrolling, and the readout measures it rather than assuming it.

  • 1.4.4 Resize Text Level AA

    At 200% zoom (640 pixels) every part of the order page still shows and works; text grows with the zoom because nothing is sized in fixed boxes.

  • 1.3.2 Meaningful Sequence Level A

    Columns stack in source order, with no CSS order or grid placement, so the reading order at 320 pixels is the order in the code.

  • 2.1.1 Keyboard Level A

    The slider, the zoom buttons and the page's controls all work from the keyboard, and the preview takes focus while it scrolls so the arrow keys can scroll it.

  • 4.1.2 Name, Role, Value Level A

    The slider says its width and zoom in aria-valuetext, and the zoom buttons report pressed or not pressed.

  • 4.1.3 Status Messages Level AA

    The result, a copied link and saved instructions are status messages, read out without moving focus.

Usage

When to use it

Use it

  • Every page: reflow is a Level AA requirement, and it is what people who zoom to 400% depend on.
  • Testing a layout: set a 1280-pixel window to 400% zoom, or a 320-pixel viewport, and check nothing scrolls sideways.

Use something else

  • Forcing one column on content that needs two dimensions: data tables, maps, diagrams and code may scroll sideways inside their own focusable region.
  • Hiding what does not fit: a narrow layout must keep every piece of content and every function, even if it moves.

Common failures

How it usually goes wrong

  • A fixed page width

    width: 960px or a min-width on a wrapper makes every line scroll sideways at 400% zoom. Turn on the fixed-width version above and narrow the preview to see it.

  • Long links and addresses that cannot break

    One unbroken URL or email address pushes the whole page wider than the screen. overflow-wrap: anywhere on them lets them wrap at any character.

  • overflow-x: hidden as the fix

    Clipping the page hides the scrollbar but also the content past the edge, which is worse than scrolling. Make the content fit instead.

  • Rows that never wrap

    A toolbar or a row of figures with white-space: nowrap or fixed flex sizes runs off the edge. flex-wrap and auto-fit grids let them fold onto new lines.

  • Reordering columns to stack them

    CSS order or grid placement can put the sidebar first on a phone while the code still reads it last, so screen reader and keyboard order no longer match the screen.

  • Zoom turned off

    maximum-scale=1 or user-scalable=no in the viewport meta tag stops people zooming at all on some phones. Leave zoom alone and make the layout cope.

Notes

Building it

  • The parts to copy into a real page are the rules for the order page (.ap-reflow__page and what is inside it); the slider, the readout and the scaled preview are a test bench for this demo.
  • The page is styled mobile first and widened with container queries, so each part reflows wherever it is placed; on a whole page, media queries on the viewport work too, because at 400% zoom the viewport really is 320 CSS pixels wide.
  • The preview is laid out at the chosen CSS width and then scaled to fit this box with a transform, which never changes layout, so the readout's scrollWidth is the real content width.
  • The order page has no landmarks because it sits inside this page's main landmark; on a real page it would be the main content, under the site's own header and navigation. The tracking link stands for another page, so it has a # href here.
  • To check your own pages, set a 1280-pixel window to 400% zoom, or 320 pixels in your browser's responsive mode, and confirm document.documentElement.scrollWidth is no wider than clientWidth.

Sources: WCAG 2.2 Understanding: Reflow · WCAG 2.2 Understanding: Resize Text · MDN: CSS container queries

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction