# Trim the Text Box

**SHOULD** · **ID:** `design-text-box-trim` · **Category:** design
**Source:** [jakubkrehel](https://github.com/jakubkrehel/skills)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-text-box-trim

> SHOULD: Use `text-box: trim-both cap alphabetic` to remove the font's reserved space and half-leading, so symmetric padding becomes optically symmetric instead of leaving text riding low in badges, chips and short buttons. This is progressive enhancement — ship pre-tuned padding as the base and gate the trim (plus the symmetric padding it enables) behind `@supports`; the layout must be correct without it.

Symmetric padding does not optically centre text, because the font reserves uneven space above and below the letters — trim it with text-box

A line box is not the same height as the letters in it. The font declares an ascent and a descent that leave room for accents and descenders, and the line-height adds half-leading above and below on top of that. Neither is symmetric around the cap height, so when you give a badge `py-2` on both sides, the text does NOT land in the optical middle — it sits a pixel or two low, and every badge, chip and short button in the product carries the same small wrongness. The usual fix is to fudge it: `pt-1.5 pb-2`, tuned by eye, per component, per font, and silently invalidated the moment the typeface changes. `text-box: trim-both cap alphabetic` fixes the cause instead — it tells the browser to measure the box from the cap height to the baseline and throw away the reserved space outside it, so symmetric padding becomes optically symmetric. Support is still limited, so this is genuinely progressive enhancement: ship the pre-tuned padding as the base, and let an `@supports (text-box: trim-both cap alphabetic)` block reset the padding to symmetric and apply the trim. The layout must be correct without it.

## Rule snippet

```tsx
.badge { padding-top: 0.375rem; padding-bottom: 0.5rem; }
@supports (text-box: trim-both cap alphabetic) {
  .badge { text-box: trim-both cap alphabetic; padding-block: 0.5rem; }
}
```

## Bad — do not do this

`design-text-box-trim-bad`

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

export function TextBoxTrimBad() {
  const [guides, setGuides] = useState(true);

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between gap-3">
        <p className="text-xs text-muted-foreground">Symmetric padding (py-2), no trim</p>
        <button
          type="button"
          onClick={() => setGuides((v) => !v)}
          aria-pressed={guides}
          className="rounded-md border border-border bg-muted px-3 py-1.5 text-xs font-medium text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
        >
          Guides: {guides ? 'on' : 'off'}
        </button>
      </div>

      <div className="flex flex-wrap items-start gap-4 rounded-lg border border-border bg-card p-6">
        <div className="relative">
          <span className="inline-flex rounded-md bg-primary px-3 py-2 text-2xl font-semibold leading-none text-primary-foreground">
            Beta
          </span>
          {guides && (
            <>
              {/* Geometric centre of the padding box. */}
              <div
                aria-hidden="true"
                className="pointer-events-none absolute inset-x-[-14px] top-1/2 h-px bg-destructive"
              />
              <span
                aria-hidden="true"
                className="pointer-events-none absolute -right-2 top-1/2 translate-x-full -translate-y-1/2 whitespace-nowrap text-[10px] text-destructive"
              >
                box centre
              </span>
            </>
          )}
        </div>

        <div className="relative">
          <button
            type="button"
            className="rounded-md border border-border bg-muted px-4 py-2 text-2xl font-semibold leading-none text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            Deploy
          </button>
          {guides && (
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-x-[-14px] top-1/2 h-px bg-destructive"
            />
          )}
        </div>
      </div>

      <p className="text-xs text-destructive">
        Padding is symmetric, so the box is centred — but the text is not. The font reserves space above
        the cap height and below the baseline for accents and descenders, and that reservation is not
        symmetric, so the glyphs land visibly below the box centre. Every badge, chip and short button in
        the product carries the same small wrongness, and the usual fix (hand-tuning{' '}
        <code>pt-1.5 pb-2</code> per component) is invalidated the moment the typeface changes.
      </p>
    </div>
  );
}
```

## Good — do this

`design-text-box-trim-good`

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

export function TextBoxTrimGood() {
  const [guides, setGuides] = useState(true);
  const [supported, setSupported] = useState<boolean | null>(null);

  useEffect(() => {
    setSupported(
      typeof CSS !== 'undefined' && CSS.supports('text-box', 'trim-both cap alphabetic')
    );
  }, []);

  return (
    <div className="space-y-3">
      <style>{`
        /* Base: pre-tuned asymmetric padding, so the layout is already correct
           in every browser that has never heard of text-box. */
        .tbt-chip {
          padding-block: 0.42rem 0.58rem;
        }

        /* Enhancement: trim the reserved space, then let padding go symmetric. */
        @supports (text-box: trim-both cap alphabetic) {
          .tbt-chip {
            text-box: trim-both cap alphabetic;
            padding-block: 0.5rem;
          }
        }
      `}</style>

      <div className="flex items-center justify-between gap-3">
        <p className="text-xs text-muted-foreground">
          text-box: trim-both cap alphabetic{' '}
          {supported === null ? '' : supported ? '— supported here' : '— not supported here, base padding is doing the work'}
        </p>
        <button
          type="button"
          onClick={() => setGuides((v) => !v)}
          aria-pressed={guides}
          className="rounded-md border border-border bg-muted px-3 py-1.5 text-xs font-medium text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
        >
          Guides: {guides ? 'on' : 'off'}
        </button>
      </div>

      <div className="flex flex-wrap items-start gap-4 rounded-lg border border-border bg-card p-6">
        <div className="relative">
          <span className="tbt-chip inline-flex rounded-md bg-primary px-3 text-2xl font-semibold leading-none text-primary-foreground">
            Beta
          </span>
          {guides && (
            <>
              <div
                aria-hidden="true"
                className="pointer-events-none absolute inset-x-[-14px] top-1/2 h-px bg-success"
              />
              <span
                aria-hidden="true"
                className="pointer-events-none absolute -right-2 top-1/2 translate-x-full -translate-y-1/2 whitespace-nowrap text-[10px] text-success"
              >
                box centre
              </span>
            </>
          )}
        </div>

        <div className="relative">
          <button
            type="button"
            className="tbt-chip rounded-md border border-border bg-muted px-4 text-2xl font-semibold leading-none text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            Deploy
          </button>
          {guides && (
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-x-[-14px] top-1/2 h-px bg-success"
            />
          )}
        </div>
      </div>

      <p className="text-xs text-success">
        The cap-to-baseline band now straddles the box centre, because the browser measures the box from
        the cap height (<code>cap</code>) to the baseline (<code>alphabetic</code>) and discards the
        reserved space outside it — so symmetric padding becomes optically symmetric.{' '}
        <strong>Support is still limited</strong>, so this is genuine progressive enhancement: the
        pre-tuned padding ships as the base and the <code>@supports</code> block resets it to symmetric
        only where the trim actually lands. The layout must be correct without it.
      </p>
    </div>
  );
}
```

## References

- [jakubkrehel/skills — better-typography: spacing & sizing](https://github.com/jakubkrehel/skills/blob/main/skills/better-typography/spacing-and-sizing.md)
- [MDN: text-box](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-box)
