# Underlines From the Font

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

> SHOULD: Take underline geometry from the font: `text-underline-position: from-font`, `text-decoration-thickness: from-font`, `text-decoration-skip-ink: auto` — the browser's default position slices through the descenders of g, y, p and j. Colour is the only part of a real `text-decoration` that animates reliably, so transition `text-decoration-color` for a colour hover and build anything richer (a wipe, a grow, a slide) as a separate element animated on `transform: scaleX()`.

Take underline position and thickness from the font's own metrics, and build any animated underline as a separate element

Two halves, and the second is the one people miss. The first is placement: a default underline is drawn at a position the browser picks, which at large sizes and in serif faces slices straight through the descenders of g, y, p and j. A well-made font ships its own underline position and thickness in its metrics, and `text-underline-position: from-font` plus `text-decoration-thickness: from-font` tells the browser to use them; `text-decoration-skip-ink: auto` then breaks the line around any descender that still crosses it. The second half is animation. `text-decoration-thickness`, `text-underline-offset` and `text-decoration-line` are not reliably animatable — a hover transition on them either does nothing or steps in visible jumps. Color is the exception, and the only exception. So the rule is conditional, not absolute: if the hover effect is a color change, use a real `text-decoration` and transition `text-decoration-color`; the moment the effect is a wipe, a grow, or a slide, the underline has to be a separate element (a `::after` box, or a `background-image` line) that you can animate on `transform: scaleX()` — which is compositor-friendly, unlike anything `text-decoration` could have given you. A dotted underline via `text-decoration-style` remains the conventional hint that a word carries extra information, which is why `abbr` gets `text-decoration: underline dotted` by default.

## Rule snippet

```tsx
a { text-underline-position: from-font; text-decoration-thickness: from-font; text-decoration-skip-ink: auto; transition: text-decoration-color 150ms; }
```

## Bad — do not do this

`design-underline-from-font-bad`

```tsx
export function UnderlineFromFontBad() {
  return (
    <div className="space-y-3">
      <style>{`
        .ufb-link {
          /* Browser-decided underline: sits where it lands, at whatever thickness. */
          text-decoration-line: underline;
          text-decoration-skip-ink: none;
          text-underline-offset: 0;
          text-decoration-thickness: 2px;
          /* Wishful thinking: thickness is not reliably animatable. */
          transition: text-decoration-thickness 200ms ease-out;
        }
        .ufb-link:hover,
        .ufb-link:focus-visible {
          text-decoration-thickness: 5px;
        }
      `}</style>

      <div className="rounded-lg border border-border bg-card p-5">
        <p className="font-serif text-2xl leading-snug text-foreground">
          Read the{' '}
          <a
            href="#design-underline-from-font"
            className="ufb-link text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            typography guidelines
          </a>{' '}
          before you ship.
        </p>
      </div>

      <p className="text-xs text-destructive">
        The line is drawn straight through the descenders of <span className="font-serif">g</span>,{' '}
        <span className="font-serif">y</span> and <span className="font-serif">p</span> — nothing told the
        browser to use the font&apos;s own underline metrics, and{' '}
        <code>text-decoration-skip-ink: none</code> stops it breaking around them. Hover it: the
        thickness transition either snaps or does nothing at all, because thickness is not a reliably
        animatable property.
      </p>
    </div>
  );
}
```

## Good — do this

`design-underline-from-font-good`

```tsx
export function UnderlineFromFontGood() {
  return (
    <div className="space-y-3">
      <style>{`
        /* 1. Colour-only hover → a real underline is fine, taken from the font's metrics. */
        .ufg-metric {
          text-decoration-line: underline;
          text-underline-position: from-font;
          text-decoration-thickness: from-font;
          text-decoration-skip-ink: auto;
          --underline-ink: var(--muted-foreground);
          text-decoration-color: var(--underline-ink);
          transition: text-decoration-color 200ms ease-out;
        }
        .ufg-metric:hover,
        .ufg-metric:focus-visible {
          --underline-ink: var(--foreground);
        }

        /* 2. Anything richer than colour → a separate element, animated on transform. */
        .ufg-anim {
          position: relative;
          text-decoration-line: none;
        }
        .ufg-anim::after {
          content: '';
          position: absolute;
          inset-inline: 0;
          bottom: -0.12em;
          height: 2px;
          background-image: linear-gradient(currentColor, currentColor);
          transform: scaleX(0);
          transform-origin: left center;
          transition: transform 220ms ease-out;
        }
        .ufg-anim:hover::after,
        .ufg-anim:focus-visible::after {
          transform: scaleX(1);
        }
        @media (prefers-reduced-motion: reduce) {
          .ufg-anim::after { transition: none; }
        }

        /* Bonus: the conventional hint that a word carries extra information. */
        .ufg-scope abbr {
          text-decoration: underline dotted;
          cursor: help;
        }
      `}</style>

      <div className="ufg-scope space-y-4 rounded-lg border border-border bg-card p-5">
        <p className="font-serif text-2xl leading-snug text-foreground">
          Read the{' '}
          <a
            href="#design-underline-from-font"
            className="ufg-metric focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            typography guidelines
          </a>{' '}
          before you ship.
        </p>

        <p className="font-serif text-2xl leading-snug text-foreground">
          Or skim the{' '}
          <a
            href="#design-underline-from-font"
            className="ufg-anim focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            typography glossary
          </a>{' '}
          instead.
        </p>

        <p className="text-sm text-muted-foreground">
          A dotted underline still means &ldquo;there is more here&rdquo;: <abbr title="Cumulative Layout Shift">CLS</abbr>{' '}
          and <abbr title="Largest Contentful Paint">LCP</abbr>.
        </p>
      </div>

      <p className="text-xs text-success">
        Line 1 pulls position and thickness from the font (<code>from-font</code>) and breaks around
        descenders (<code>skip-ink: auto</code>); its hover animates{' '}
        <code>text-decoration-color</code>, the one part of a real underline that animates reliably. Line
        2 wants a wipe, not a colour change, so the underline is a separate <code>::after</code> element
        animated on <code>transform: scaleX()</code> — which actually moves, on the compositor.
      </p>
    </div>
  );
}
```

## References

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