---
title: "Dynamic Type"
description: "The reader's own text-size multiplier, the 1.3× bar every component must clear without breaking, and the layout habits that decide whether it does."
url: "https://opsinjs.pensievelabs.org/foundations/typography/dynamic-type"
source: "https://opsinjs.pensievelabs.org/foundations/typography/dynamic-type.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["dynamic type", "text size multiplier", "font scaling", "larger text"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="foundation" />

## Overview [#overview]

Dynamic Type is the name for the reader deciding how big text should be. Every
platform has it, most people who need it have already set it, and almost every
web interface ignores it.

For a consumer health product this is not a minor accommodation. The people most
likely to be checking a blood-pressure reading are disproportionately likely to
have set a larger text size, and they set it once, years ago, and then expect
every app to work. An interface that clips, truncates or overlaps at their
setting is not "slightly off" for them. It is the version of the product they
have, permanently.

The requirement in opsinjs is stated as a bar rather than an aspiration:
**every component must survive a 1.3× multiplier with no clipping, no truncation,
no overlap and no horizontal scroll**, and the whole system must remain usable at
200%. 1.3× is chosen because it is unremarkable. It is well within the range
people actually use, unlike 200%, which tends to be treated as a stunt and
therefore excused.

## How it works [#how-it-works]

**Scaling is a real font-size change, not a transform.** The docs previews and
the `(view)` routes implement the reader's multiplier by changing the root font
size, exactly as the platform does. That means text reflows, containers grow,
line counts change and layouts have to cope. A `transform: scale()` would look
similar in a screenshot and would prove nothing, because it cannot produce the
failure being tested for.

**Every size in the system is relative.** Type roles resolve to `rem`, spacing
resolves through Tailwind's `--spacing`, and radii resolve to `rem`. A single
`px` font size anywhere in a component is a component that has opted out.

**Every role scales by the same factor.** The multiplier is applied once, to the
root font size, so a caption and a title grow in the same proportion and the
relationships inside the scale hold at every size. This is the right trade for a
health product. A reader who has already set a larger system text size, and most
of the people checking a reading have, keeps that exact setting through, because
each role resolves to `rem`; the product does not second-guess it by tuning
roles against each other. The argument is on [Type scale](./type-scale.mdx).

**Touch targets do not shrink and do not stretch.** The 44pt floor from
[Density and touch](../space/density-and-touch.mdx) is a floor at every text
size. A control's hit area grows with its label, never the reverse.

**Text scaling and browser zoom are different mechanisms** and both must work.
Zoom scales everything including layout; text scaling changes only text, which is
the harder case because it changes the *ratio* between text and everything around
it. A layout that survives 400% zoom can still fail at 1.3× text.

<ComponentPreview name="result-card" />

## Using it [#using-it]

The failures are predictable and there are five of them. Every one is caused by a
layout habit that is fine at 1× and wrong above it.

**Fixed heights.** A card with a set height clips its own content the moment the
text inside it needs another line. Use minimum heights and let content decide.

**Single-line assumptions.** `white-space: nowrap` on a label, a truncation with
an ellipsis, a value and a unit expected to sit on one line. At 1.3× the label
wraps, and if it cannot wrap it either overflows or disappears.

**Horizontal pairs that must stay horizontal.** A row of label and value works at
1×, needs to become a stack at 1.3×, and cannot decide that from the viewport
width because the container may be a narrow tile on a wide screen. This is
container-query territory. See
[Responsive modes](../space/responsive-modes.mdx).

**Icons sized in pixels beside text.** An icon that does not grow with its label
looks progressively more wrong and eventually stops reading as part of the same
element. Icons in text contexts size in `em`. The rule is on
[Sizing and alignment](../iconography/sizing-and-alignment.mdx).

**Sticky chrome that eats the viewport.** A sticky header and a pinned action bar
at 1.3× on a small phone can leave a strip of content between them. Both must
collapse rather than persist at full height.

<DoDont>
  <DoDont.Do>
    A metric tile with a minimum height, a label allowed to wrap to two lines, and
    a container query that stacks the value beneath the label when the tile is
    narrow relative to its content. It grows; nothing is lost.
  </DoDont.Do>

  <DoDont.Dont>
    The same tile with a fixed height and a truncated label. At 1.3× the reader
    sees `Resting heart ra…` above a number with no unit, which is not a degraded
    experience. It is a health value with its meaning removed.
  </DoDont.Dont>
</DoDont>

**Test it as a habit, not as an audit.** Every `<ComponentPreview>` on this site
carries a text-size switch for exactly this reason: checking 1.3× should cost one
click while you are building, not a scheduled review three weeks before release.

**Never truncate a value, a unit or a status word.** If something has to be cut,
it is not one of those three. A truncated unit turns a measurement into a
different measurement.

## Tokens [#tokens]

The multiplier steps used by the preview switch, and the size, line height,
tracking and weight of every role, are generated from `tokens/type.json` by
`scripts/build-tokens.mts`. The steps multiply the rem value of every role by the
same factor; they do not change the leading ratios, which are already
multipliers.

<TokenTable scope="type" />

## Accessibility impact [#accessibility-impact]

* **WCAG 2.2 SC 1.4.4 *Resize Text* requires 200% without loss of content or
  functionality**, and SC 1.4.10 *Reflow* requires no two-dimensional scrolling
  at 320 CSS pixels of width. The 1.3× bar is stricter in practice than either,
  because it is applied to every component rather than to a page as a whole.
* **1.3× is the working bar and 200% is the conformance bar.** Components are
  built and reviewed at 1.3×; the system is verified at 200%. Anything that fails
  at 1.3× would fail catastrophically at 200%, so the earlier check catches more.
* **Truncation is content loss.** An ellipsis at a larger text size fails SC
  1.4.4 as surely as a clipped container does, and it is the failure most likely
  to survive review because it looks deliberate.
* **This interacts with every other foundation.** Space decides whether there is
  room, shape decides whether the corners still look right at a larger radius,
  iconography decides whether the glyph kept up, and layout decides whether the
  shell still fits. Dynamic Type is where the whole system is tested at once.
* **The docs site is held to the same bar**, and the result is published on
  [Text resizing and zoom](../../accessibility/text-resizing-and-zoom.mdx) and
  [This site's accessibility](../../accessibility/this-site.mdx).

## Related [#related]

* [Type scale](./type-scale.mdx) explains why a scale named for jobs lets every
  role scale together and keeps the reader's own setting.
* [Responsive modes](../space/responsive-modes.mdx) covers container queries,
  and why a component cannot decide its layout from the viewport.
* [Text resizing and zoom](../../accessibility/text-resizing-and-zoom.mdx) has
  the conformance position, the test steps and the measured results.
