# Survive User Text Spacing

**MUST** · **ID:** `content-text-spacing-override` · **Category:** content
**Source:** [WCAG](https://www.w3.org/WAI/WCAG21/quickref/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/content-text-spacing-override

> MUST: Text containers must survive user-applied `line-height: 1.5`, `letter-spacing: 0.12em`, `word-spacing: 0.16em` and 2× paragraph spacing with no loss of content — WCAG SC 1.4.12 Text Spacing, Level AA. The failure is the box, not the type: fixed `height` + `overflow: hidden` slices the last line off. Size with `min-height` and padding, let flex/grid tracks size to content, and never clip text you mean to be read.

Size text containers so nothing is lost when the user overrides line, letter, word, and paragraph spacing

SC 1.4.12 Text Spacing (Level AA) is not about the spacing you choose — content-ibelick-letter-spacing and content-impeccable-tight-leading cover that. It is about what happens when a reader with low vision or dyslexia overrides your choice with a user stylesheet, a bookmarklet, or a reading extension, and sets line-height to 1.5x the font size, spacing after paragraphs to 2x, letter-spacing to 0.12em, and word-spacing to 0.16em, all at once. The typical failure is not the type: it is the box. A container with a fixed height plus overflow: hidden cannot grow, so the last line is sliced off mid-glyph and the content is simply gone — a "no loss of content" failure, not a cosmetic one. Fix it by sizing with min-height and padding instead of height, letting flex and grid tracks size to content, and never clipping text you intend the user to read. Test it by applying the four values to the whole page and looking for clipped, truncated, or overlapping text.

## Rule snippet

```tsx
/* clips at 1.5 line-height */ .card { height: 96px; overflow: hidden; }
/* grows with the user's spacing */ .card { min-height: 96px; padding-block: 12px; }
```

## Bad — do not do this

`content-text-spacing-override-bad`

```tsx
import { useState } from 'react';

/** The four values a user (or a bookmarklet, or a reading extension) may set under SC 1.4.12. */
const WCAG_TEXT_SPACING = {
  lineHeight: 1.5,
  letterSpacing: '0.12em',
  wordSpacing: '0.16em',
} as const;

export function TextSpacingOverrideBad() {
  const [overridden, setOverridden] = useState(false);

  const spacing = overridden ? WCAG_TEXT_SPACING : undefined;
  const paragraphSpacing = overridden ? { marginBottom: '2em' } : undefined;

  return (
    <div className="w-full max-w-sm">
      <button
        type="button"
        aria-pressed={overridden}
        onClick={() => setOverridden((v) => !v)}
        className="w-full mb-4 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-muted text-foreground hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {overridden ? 'Remove user text spacing' : 'Apply user text spacing (SC 1.4.12)'}
      </button>

      <div className="bg-card border border-border rounded-lg p-4 space-y-4">
        {/* Fixed height + overflow hidden: the box cannot grow, so the text is cut. */}
        <div>
          <p className="text-xs text-muted-foreground mb-1 font-mono">height: 40px; overflow: hidden</p>
          <div
            style={{ height: 40, overflow: 'hidden', ...spacing }}
            className="w-44 px-3 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
          >
            Save and continue
          </div>
        </div>

        <div>
          <p className="text-xs text-muted-foreground mb-1 font-mono">height: 100px; overflow: hidden</p>
          <div
            style={{ height: 100, overflow: 'hidden', ...spacing }}
            className="rounded-lg bg-muted p-3 text-sm text-foreground"
          >
            <p style={paragraphSpacing}>
              Your plan renews on 1 August. Cancel any time before then to avoid the next charge.
            </p>
            <p>Invoices are emailed to the billing contact.</p>
          </div>
        </div>
      </div>

      <p className="text-xs text-destructive mt-4">
        {overridden
          ? 'Text is clipped mid-line: the fixed height cannot grow, so content is lost. SC 1.4.12 fails.'
          : 'Looks fine at author spacing — turn the override on to see the content disappear.'}
      </p>
    </div>
  );
}
```

## Good — do this

`content-text-spacing-override-good`

```tsx
import { useState } from 'react';

/** The four values a user (or a bookmarklet, or a reading extension) may set under SC 1.4.12. */
const WCAG_TEXT_SPACING = {
  lineHeight: 1.5,
  letterSpacing: '0.12em',
  wordSpacing: '0.16em',
} as const;

export function TextSpacingOverrideGood() {
  const [overridden, setOverridden] = useState(false);

  const spacing = overridden ? WCAG_TEXT_SPACING : undefined;
  const paragraphSpacing = overridden ? { marginBottom: '2em' } : undefined;

  return (
    <div className="w-full max-w-sm">
      <button
        type="button"
        aria-pressed={overridden}
        onClick={() => setOverridden((v) => !v)}
        className="w-full mb-4 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-muted text-foreground hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {overridden ? 'Remove user text spacing' : 'Apply user text spacing (SC 1.4.12)'}
      </button>

      <div className="bg-card border border-border rounded-lg p-4 space-y-4">
        {/* min-height + padding: the box grows with its text instead of cropping it. */}
        <div>
          <p className="text-xs text-muted-foreground mb-1 font-mono">min-height: 40px; padding: 8px 12px</p>
          <div
            style={{ minHeight: 40, ...spacing }}
            className="w-44 px-3 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
          >
            Save and continue
          </div>
        </div>

        <div>
          <p className="text-xs text-muted-foreground mb-1 font-mono">min-height: 100px; padding: 12px</p>
          <div
            style={{ minHeight: 100, ...spacing }}
            className="rounded-lg bg-muted p-3 text-sm text-foreground"
          >
            <p style={paragraphSpacing}>
              Your plan renews on 1 August. Cancel any time before then to avoid the next charge.
            </p>
            <p>Invoices are emailed to the billing contact.</p>
          </div>
        </div>
      </div>

      <p className="text-xs text-success mt-4">
        {overridden
          ? 'Every word survives: the containers grew to fit line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em, and 2em between paragraphs.'
          : 'Same content, sized with min-height — turn the override on and nothing is lost.'}
      </p>
    </div>
  );
}
```

## References

- [Understanding SC 1.4.12: Text Spacing](https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html)
- [MDN: min-height](https://developer.mozilla.org/en-US/docs/Web/CSS/min-height)
