Accessibility pattern · Interaction and motion

Text size and spacing

People who read more easily with wider spacing override it with their own styles; 1.4.12 asks that nothing is lost when they do. The robust card grows with its text because nothing in it has a fixed height; the fragile one uses fixed heights and overflow: hidden, and the script counts what it cuts off.

WCAG criteria
6
Keyboard rules
5
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.

100%
Push it further

Each value is a multiple of the font size; each slider starts at the 1.4.12 minimum.

1.5×
2×
0.12×
0.16×

Robust

Clipped or overlapping: 0

Heights grow with the text, and rows wrap.

New guide

Cooking for a crowd on a budget

Plan a thali for twelve without the stress: a shopping list, a timeline and the dishes to make the day before.

Read the guide

Sent every Friday.

Fragile

Clipped or overlapping: 0

Fixed heights and widths, with overflow: hidden.

New guide

Cooking for a crowd on a budget

Plan a thali for twelve without the stress: a shopping list, a timeline and the dishes to make the day before.

Read the guide

Sent every Friday.

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
Tab or ShiftTabMoves between the spacing switch, the fragile version checkbox, the text size slider and, while spacing is on, the four spacing sliders.
SpaceOn the switch, applies the 1.4.12 values or removes them; on the checkbox, shows or hides the fragile card.
Arrow Right or Arrow UpOn a slider, widens that spacing or enlarges the text by one step.
Arrow Left or Arrow DownOn a slider, narrows it again; spacing never goes below the 1.4.12 minimum.
Home or EndOn a slider, jumps to the minimum or the maximum.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches the switchApply 1.4.12 spacing, switch, off
Space turns it onOn. 1.4.12 spacing applied. Robust card, clipped or overlapping: 0. Fragile card, clipped or overlapping: 6.
Tab reaches the first sliderLine height, slider, 1.5. Each value is a multiple of the font size; each slider starts at the 1.4.12 minimum.
Arrow Right on it1.6
Space turns the switch offOff. Spacing back to normal.

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-text-spacing" data-ap-text-spacing>
  <div class="ap-text-spacing__controls">
    <div class="ap-text-spacing__main">
      <label class="ap-text-spacing__switch">
        <input class="ap-text-spacing__switch-input" type="checkbox" role="switch" data-ap-apply />
        <span class="ap-text-spacing__track" aria-hidden="true"><span class="ap-text-spacing__thumb"></span></span>
        <span>Apply 1.4.12 spacing</span>
      </label>
      <label class="ap-text-spacing__check">
        <input type="checkbox" checked data-ap-show-fragile />
        <span>Show the fragile version</span>
      </label>
      <div class="ap-text-spacing__size">
        <label class="ap-text-spacing__slider-label" for="text-spacing-size">Text size</label>
        <input class="ap-text-spacing__range" id="text-spacing-size" type="range" min="100" max="200" step="10" value="100" aria-valuetext="100%" data-ap-size />
        <span class="ap-text-spacing__val" translate="no" data-ap-size-val>100%</span>
      </div>
    </div>
    <fieldset class="ap-text-spacing__sliders" data-ap-sliders disabled>
      <legend class="ap-text-spacing__legend">Push it further</legend>
      <p class="ap-text-spacing__units" id="text-spacing-units">Each value is a multiple of the font size; each slider starts at the 1.4.12 minimum.</p>
      <div class="ap-text-spacing__grid">
        <div class="ap-text-spacing__slider">
          <label class="ap-text-spacing__slider-label" for="text-spacing-line">Line height</label>
          <input class="ap-text-spacing__range" id="text-spacing-line" type="range" min="1.5" max="3" step="0.1" value="1.5" aria-describedby="text-spacing-units" data-ap-prop="line" />
          <span class="ap-text-spacing__val" translate="no" data-ap-val>1.5×</span>
        </div>
        <div class="ap-text-spacing__slider">
          <label class="ap-text-spacing__slider-label" for="text-spacing-para">Space after paragraphs</label>
          <input class="ap-text-spacing__range" id="text-spacing-para" type="range" min="2" max="4" step="0.25" value="2" aria-describedby="text-spacing-units" data-ap-prop="para" />
          <span class="ap-text-spacing__val" translate="no" data-ap-val>2×</span>
        </div>
        <div class="ap-text-spacing__slider">
          <label class="ap-text-spacing__slider-label" for="text-spacing-letter">Letter spacing</label>
          <input class="ap-text-spacing__range" id="text-spacing-letter" type="range" min="0.12" max="0.4" step="0.02" value="0.12" aria-describedby="text-spacing-units" data-ap-prop="letter" />
          <span class="ap-text-spacing__val" translate="no" data-ap-val>0.12×</span>
        </div>
        <div class="ap-text-spacing__slider">
          <label class="ap-text-spacing__slider-label" for="text-spacing-word">Word spacing</label>
          <input class="ap-text-spacing__range" id="text-spacing-word" type="range" min="0.16" max="0.8" step="0.04" value="0.16" aria-describedby="text-spacing-units" data-ap-prop="word" />
          <span class="ap-text-spacing__val" translate="no" data-ap-val>0.16×</span>
        </div>
      </div>
    </fieldset>
    <p class="ap-text-spacing__sr" role="status" data-ap-spacing-status></p>
  </div>

  <div class="ap-text-spacing__previews" data-ap-previews>
      <div class="ap-text-spacing__panel ap-text-spacing__panel--robust" role="group" aria-labelledby="text-spacing-robust-name" data-ap-panel="robust">
        <div class="ap-text-spacing__panel-top">
          <h3 class="ap-text-spacing__panel-name" id="text-spacing-robust-name">Robust</h3>
          <p class="ap-text-spacing__verdict" data-ap-verdict data-state="ok"><svg class="ap-text-spacing__ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg><svg class="ap-text-spacing__bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg><span>Clipped or overlapping:</span> <span translate="no" data-ap-clipped>0</span></p>
        </div>
        <p class="ap-text-spacing__panel-desc">Heights grow with the text, and rows wrap.</p>
        <div class="ap-text-spacing__sample ap-text-spacing__sample--robust" data-ap-sample inert>
          <div class="ap-text-spacing__nav">
            <a href="#recipes">Recipes</a>
            <a href="#guides">Guides</a>
            <a href="#videos">Videos</a>
            <a href="#saved">Saved</a>
          </div>
          <div class="ap-text-spacing__teaser">
            <span class="ap-text-spacing__badge">New guide</span>
            <h4 class="ap-text-spacing__teaser-name">Cooking for a crowd on a budget</h4>
            <p class="ap-text-spacing__teaser-text">Plan a thali for twelve without the stress: a shopping list, a timeline and the dishes to make the day before.</p>
            <a class="ap-text-spacing__more" href="#guide">Read the guide</a>
          </div>
          <div class="ap-text-spacing__field">
            <label class="ap-text-spacing__label" for="text-spacing-robust-email">Email for the weekly menu</label>
            <p class="ap-text-spacing__hint">Sent every Friday.</p>
            <input class="ap-text-spacing__input" id="text-spacing-robust-email" type="email" autocomplete="email" />
          </div>
          <div class="ap-text-spacing__actions">
            <button type="button" class="ap-btn ap-btn--primary">Subscribe</button>
            <button type="button" class="ap-btn">Not now</button>
          </div>
        </div>
      </div>
      <div class="ap-text-spacing__panel ap-text-spacing__panel--fragile" role="group" aria-labelledby="text-spacing-fragile-name" data-ap-panel="fragile">
        <div class="ap-text-spacing__panel-top">
          <h3 class="ap-text-spacing__panel-name" id="text-spacing-fragile-name">Fragile</h3>
          <p class="ap-text-spacing__verdict" data-ap-verdict data-state="ok"><svg class="ap-text-spacing__ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg><svg class="ap-text-spacing__bad" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg><span>Clipped or overlapping:</span> <span translate="no" data-ap-clipped>0</span></p>
        </div>
        <p class="ap-text-spacing__panel-desc">Fixed heights and widths, with overflow: hidden.</p>
        <div class="ap-text-spacing__sample ap-text-spacing__sample--fragile" data-ap-sample inert>
          <div class="ap-text-spacing__nav">
            <a href="#recipes">Recipes</a>
            <a href="#guides">Guides</a>
            <a href="#videos">Videos</a>
            <a href="#saved">Saved</a>
          </div>
          <div class="ap-text-spacing__teaser">
            <span class="ap-text-spacing__badge">New guide</span>
            <h4 class="ap-text-spacing__teaser-name">Cooking for a crowd on a budget</h4>
            <p class="ap-text-spacing__teaser-text">Plan a thali for twelve without the stress: a shopping list, a timeline and the dishes to make the day before.</p>
            <a class="ap-text-spacing__more" href="#guide">Read the guide</a>
          </div>
          <div class="ap-text-spacing__field">
            <label class="ap-text-spacing__label" for="text-spacing-fragile-email">Email for the weekly menu</label>
            <p class="ap-text-spacing__hint">Sent every Friday.</p>
            <input class="ap-text-spacing__input" id="text-spacing-fragile-email" type="email" autocomplete="email" />
          </div>
          <div class="ap-text-spacing__actions">
            <button type="button" class="ap-btn ap-btn--primary">Subscribe</button>
            <button type="button" class="ap-btn">Not now</button>
          </div>
        </div>
      </div>
  </div>
</div>

WCAG 2.2

What it meets

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

  • 1.4.12 Text Spacing Level AA

    With line height 1.5, paragraph spacing 2, letter spacing 0.12 and word spacing 0.16 times the font size, the robust card loses nothing: no clipped text, no overlap.

  • 1.4.4 Resize Text Level AA

    The robust card is sized in em with no fixed heights, so the Text size slider can take it to 200 percent and nothing is lost; the fragile card, sized in pixels, clips.

  • 1.4.10 Reflow Level AA

    Rows of links and buttons wrap instead of scrolling sideways, so the card still fits a 320 pixel wide screen with the wider spacing.

  • 2.1.1 Keyboard Level A

    The switch, the checkbox and the native range sliders all work from the keyboard.

  • 4.1.2 Name, Role, Value Level A

    The switch is a checkbox with role=switch, and each slider is a labelled native range input that reports its value.

  • 4.1.3 Status Messages Level AA

    Turning the spacing on announces how many parts of each card are clipped or overlapping, so the result does not depend on seeing it.

Usage

When to use it

Use it

  • Every component with text: buttons, badges, navigation, cards, form fields and tables all meet 1.4.12 or fail it.
  • Design reviews: apply the test values to each new component before it ships, as the switch here does.

Use something else

  • Shipping a spacing control instead of fixing the layout: 1.4.12 is about people's own stylesheets and extensions, which you cannot see.
  • Text inside images or canvas: it cannot respond to spacing at all, so use real text.

Common failures

How it usually goes wrong

  • Fixed heights on text containers

    A height that fits the text today cuts it off when the line height grows. Use min-height, or let padding and content decide.

  • overflow: hidden as tidying up

    Clipping hides the failure instead of preventing it. The fragile card cuts off its links, badge, field label, hint and a button label; the robust one has no overflow: hidden at all.

  • Rows that cannot wrap

    white-space: nowrap on a row of links or buttons pushes items out of view once letters spread. flex-wrap lets them move to a new line.

  • Badges and buttons with fixed widths

    An 80-pixel badge fits "New guide" at normal spacing only. Padding around the text keeps it fitting at any spacing.

  • Line height in pixels

    line-height: 20px stays 20 pixels when the font or the spacing grows, so lines overlap. Unitless line heights scale with the text.

  • Testing with your own eyes only

    The failure appears only under someone else's styles. Apply the four values with a bookmarklet or a test stylesheet, as this demo does, on every component.

Notes

Building it

  • The demo applies the test the way a reader's own stylesheet would: the four values with !important on every element of the card. That is the stylesheet to copy for your own checks.
  • 1.4.12 asks only that nothing is lost at those values; it does not ask you to offer spacing controls. The sliders are here to show how much headroom the robust card has.
  • The script counts an element as clipped or overlapping when its content is taller or wider than its box, and outlines it, so the fragile card shows where it fails.
  • Both cards are inert specimens: their links and buttons are not reachable, because only the layout is being tested. The live result is announced instead.
  • Text size is not part of 1.4.12; resizing text is 1.4.4. The robust card sizes everything in em from one font size, which is why the Text size slider scales it whole.

Sources: WCAG 2.2 Understanding: Text Spacing · WCAG Technique C36: allowing text spacing to be overridden · WCAG Technique C35: allowing for text spacing without wrapping

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