UI/UX Atlas
Typography Intermediate

Modular Type Scale

Building a harmonious, mathematically-grounded set of type sizes that creates consistent hierarchy, speeds design decisions, and maps cleanly to design tokens.

8 min read

Interactive example · Modular type scale
DisplayDesign with rhythm39px
H1A modular scale31px
H2Harmonious sizes25px
H3Built from one ratio20px
BodyBody text sits at the base size, usually 16px, the anchor of the whole scale.16px
SmallCaptions and metadata13px

Every size is the base (16px) multiplied by the ratio raised to a step. One ratio yields a consistent, musical hierarchy instead of arbitrary sizes.

The full lesson

When designers pick font sizes freely — one uses 15px, another 17px, a third adds 22px because it “felt right” — the result is visual noise. The hierarchy breaks down, and no one quite notices why. A modular type scale fixes this by deriving every size from a single base value and a consistent ratio. You get a small, intentional set of sizes that creates clear hierarchy, looks naturally harmonious, and fits neatly into named tokens. Building and shipping a type scale is one of the highest-leverage typographic skills you can develop.

What a Modular Scale Is

A modular type scale is a sequence of sizes built by multiplying (or dividing) a base size by a fixed ratio at each step. Say your base is 1rem and your ratio is 1.25 (called the Major Third). The scale looks like this:

0.64rem  (base ÷ ratio²)
0.80rem  (base ÷ ratio¹)
1.00rem  (base — step 0)
1.25rem  (base × ratio¹)
1.563rem (base × ratio²)
1.953rem (base × ratio³)
2.441rem (base × ratio⁴)
3.052rem (base × ratio⁵)

Each step is exactly ratio times the previous step. That means proportional relationships between sizes stay consistent throughout the scale. An H2 at 1.953rem is always exactly 1.25× an H3 at 1.563rem — no manual guesswork needed.

Common Ratios and Their Character

Ratio nameValueCharacterGood for
Minor Second1.067Very tightDense data UIs, footnote ladders
Major Second1.125SubtleLong-form reading, editorial with restrained sizing
Minor Third1.200ModerateProduct UIs, dashboards
Major Third1.250BalancedMost general-purpose interfaces
Perfect Fourth1.333ExpansiveMarketing, landing pages
Augmented Fourth1.414BoldHero-heavy sites
Perfect Fifth1.500DramaticDisplay typography, posters
Golden Ratio1.618Very dramaticRarely practical — sizes diverge too fast

For most product interfaces, stick to ratios between 1.200 and 1.333. They give enough contrast between adjacent steps without making the scale unmanageably wide.

Choosing Your Base Size

The base size anchors the entire scale, so choose it to match your product’s primary reading context.

16px (1rem) is the browser default and the most common starting point. It works well for body text on screen and aligns with most users’ configured preferences.

18px–20px works better for reading-heavy products — documentation, news, blogs — because sustained reading benefits from slightly larger text.

14px–15px suits dense data UIs (dashboards, spreadsheets, admin panels) where users scan rather than read full paragraphs. Use this range with extra care: verify that every size step still passes WCAG 2.2 AA contrast requirements. The standard is 4.5:1 for normal text (under 18px regular or under 14px bold) and 3:1 for large text.

Never set your base in px in your CSS. Use rem so the scale automatically inherits the user’s browser font-size preference. A user who has increased their browser font size to compensate for low vision should see your entire scale grow proportionally — and that only works when your base is 1rem, not 16px.

Building a Scale: From Math to Tokens

Step 1: Pick your base and ratio

For a general product UI, start with 1rem base and 1.25 ratio (Major Third). This is a well-tested default. It generates enough contrast across five or six steps for clear heading hierarchy, without producing impractically large display sizes.

Step 2: Generate the full sequence

Use a tool like typescale.com, utopia.fyi, or a short script to generate the values. Name each step using a consistent convention. Two common patterns:

T-shirt sizing (intuitive for designers and engineers alike):

--font-size-xs:    0.64rem;
--font-size-sm:    0.80rem;
--font-size-base:  1.00rem;
--font-size-md:    1.25rem;
--font-size-lg:    1.563rem;
--font-size-xl:    1.953rem;
--font-size-2xl:   2.441rem;
--font-size-3xl:   3.052rem;

Numeric scale (mirrors Tailwind’s approach, easier to extend):

--font-size-100:  0.64rem;
--font-size-200:  0.80rem;
--font-size-300:  1.00rem;
--font-size-400:  1.25rem;
--font-size-500:  1.563rem;
--font-size-600:  1.953rem;
--font-size-700:  2.441rem;
--font-size-800:  3.052rem;

Whichever naming system you choose, these are primitive tokens — they list all possible values with no semantic meaning attached.

Step 3: Create semantic aliases

Primitive scale values should never appear directly in component styles. Instead, wrap them in semantic tokens that describe a role, not a size:

--font-size-body:         var(--font-size-300);
--font-size-body-sm:      var(--font-size-200);
--font-size-caption:      var(--font-size-100);
--font-size-heading-1:    var(--font-size-700);
--font-size-heading-2:    var(--font-size-600);
--font-size-heading-3:    var(--font-size-500);
--font-size-heading-4:    var(--font-size-400);
--font-size-label:        var(--font-size-200);
--font-size-display:      var(--font-size-800);

This three-tier structure — raw scale primitive → semantic role alias → component consumption — follows the W3C Design Token Community Group (DTCG) stable format using $value and $type. The payoff: you can swap the underlying ratio for a denser or more expansive scale across an entire product by changing a handful of primitive mappings. Component code doesn’t change at all.

Fluid Type Scales

A static scale defines fixed sizes. Your H1 is always 3.052rem, whether the user is on a 320px phone or a 2560px monitor. That works, but it often produces headings that feel too large on small screens or too small on large ones.

Fluid type uses clamp() to smoothly interpolate each scale step between a minimum and maximum size across a defined viewport range:

--font-size-heading-1: clamp(1.953rem, 1.2rem + 3vw, 3.052rem);

Read this as: “Never smaller than 1.953rem, never larger than 3.052rem, scaling linearly between those bounds as the viewport grows.” The middle vw-based value controls the rate of growth.

Utopia.fyi is the go-to tool for generating fluid type scales. Give it two viewport endpoints (for example, 320px min and 1440px max), your base sizes at each endpoint, and your ratio. It outputs the full clamp() series. The values maintain proportional harmony across the entire viewport range — each step is still ratio× the previous step at any given viewport width, not just at the endpoints.

Floor and zoom safety

A fluid scale that uses only vw in the middle value — like clamp(1rem, 4vw, 2rem) — has a subtle accessibility problem. When a user zooms in at 200%, the viewport width doesn’t change. So the vw component doesn’t grow with the zoom. The rem floor is the safety net: at 200% zoom, 1rem becomes 32px (assuming a 16px base), which is appropriately larger. Always set your clamp() minimum in rem, never px.

Do

  • Base all scale steps on rem so they inherit user browser-font preferences.
  • Use semantic token aliases (--font-size-heading-2) in component styles, never primitive step values directly.
  • Use clamp() with rem floors and ceilings for fluid type that respects zoom.
  • Generate the full mathematical scale, then select only the steps that map to real roles in your UI.
  • Document which scale steps are in active use and mark unused steps as reserved rather than deleting them — future roles may need them.

Don't

  • Don’t set font sizes in px — this overrides user-configured browser font-size preferences, which is a WCAG 2.2 accessibility failure.
  • Don’t use pure vw font sizes without a clamp() floor — they don’t grow during browser zoom.
  • Don’t build one static scale and apply the same sizes at every viewport width when heading sizes look wrong on mobile.
  • Don’t use “magic numbers” (15px, 17px, 22px) alongside a modular scale — every arbitrary size undermines the system’s coherence.
  • Don’t name semantic tokens after their current size (--font-size-18) — when the value changes during a ratio update, the name becomes misleading.

Pairing with Line Height and Spacing

A modular type scale doesn’t work in isolation. It becomes truly systematic when line-height and spacing are also derived from the same base unit.

Line height works best as a unitless multiplier, not a fixed pixel value. A unitless value scales automatically with font size. line-height: 1.5 on a 1rem body gives you 24px; on a 1.953rem heading it gives roughly 29px — both appropriate. Fixed line heights like 24px break whenever the font size doesn’t match.

General reference values:

  • Display / large headings: 1.1–1.2
  • Mid-level headings: 1.25–1.35
  • Body text: 1.5–1.65
  • Captions and labels: 1.3–1.4

Vertical spacing between typographic elements works best in multiples of your base line height. If your body line-height is 1.5rem (24px), using 1.5rem, 3rem, and 4.5rem as spacing units creates natural rhythm between blocks of text and the elements around them.

Adjusting Scales for Dense vs. Display Contexts

One ratio doesn’t always serve every context in a complex product.

Dense contexts — data tables, admin UIs, navigation menus — call for a smaller ratio (1.125–1.200) and a tighter base (0.875rem–1rem). Multiple information levels need to fit comfortably in constrained space. Prioritize consistency and scannability over visual drama.

Display and marketing contexts — hero sections, campaign landing pages, editorial headers — benefit from a larger ratio (1.333–1.414) and a larger base. The goal is impressive visual contrast between headline and body. A single system can have two sub-scales, one for functional UI and one for display, as long as both are mathematically grounded and mapped to named tokens.

The key constraint: don’t improvise individual sizes outside either scale. Every size used in the product should resolve to a named token from one of the defined scales.

Type Scales and Design Systems

When a type scale ships inside a design system, a few integration points matter.

Figma: Define each scale step as a Text Style. Name styles using the same semantic names as your CSS tokens (Heading/1, Body/Default, Label/Small) so the handoff is frictionless. With Figma’s Dev Mode and Code Connect, component annotations can reference the CSS custom property name directly — no separate redline documentation needed.

Storybook: Document the type scale in a dedicated story that renders every step with its token name, computed value, and intended role. This living doc stays in sync with the codebase automatically.

Token export: If your system uses a token pipeline (Style Dictionary, Theo, or a custom build step), make sure the DTCG-format JSON in your source of truth — $value, $type: "dimension" — maps cleanly to all target platforms: CSS custom properties, iOS Dynamic Type categories, Android SP values. Font sizes on iOS and Android are not a 1:1 mapping to web rem values, but the semantic role names should be identical across platforms.