Typography
Typography provides shared font roles, weight values, and fluid font sizes for text across projects and components.
Foundations provides system-font defaults for heading, body, and monospace roles so typography works without requiring project-specific fonts.
Projects can override those roles with their own typefaces. Components and project styles use the shared role tokens instead of depending directly on a particular font family.
The fluid type scale is generated with Utopia using viewport sizes, base font sizes, and scale ratios selected for each project.
Font roles
Font-family tokens describe how a typeface is used rather than tying components to a particular font family.
Foundations provides default system-font stacks:
:root {
--font-heading: system-ui, sans-serif;
--font-body: system-ui, sans-serif;
--font-mono:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace;
}These defaults allow Foundations and UI components to work without loading external fonts.
Projects can override any role with their own typeface while components continue using the same shared token.
Heading
--font-heading is used for headings and other prominent display text.
.heading {
font-family: var(--font-heading);
}The heading family controls appearance. It does not determine the semantic level of a heading.
Body
--font-body is used for paragraphs, controls, navigation, labels, and other interface text.
The base stylesheet applies it to the document body:
body {
font-family: var(--font-body);
}Monospace
--font-mono is used where fixed-width characters are meaningful, such as code, keyboard input, token names, and technical values.
code,
kbd,
samp,
pre {
font-family: var(--font-mono);
}Do not use the monospace family only to make ordinary text appear technical.
Project overrides
Override a font role when a project requires a different typeface:
:root {
--font-heading: var(--font-project-heading);
--font-body: var(--font-project-body);
--font-mono: var(--font-project-mono);
}The role tokens remain the shared typography API. Project-specific or provider-generated font variables should stay behind them.
A project does not need to override every role. For example, it can provide a custom heading family while continuing to use the Foundations defaults for body and monospace text.
Font weights
Font-weight tokens provide consistent names for the standard numeric weight values.
:root {
--font-thin: 100;
--font-extralight: 200;
--font-light: 300;
--font-regular: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--font-extrabold: 800;
}Use the token instead of repeating its numeric value:
.component-title {
font-weight: var(--font-semibold);
}A weight token does not guarantee that every configured font provides that weight. Only use weights available from the active typeface.
Use regular weight for ordinary body text. Use lighter and heavier weights intentionally to create emphasis or hierarchy.
Type scale
Generating the scale
Generate the type scale with the Utopia fluid type scale calculator.
The configuration is specific to each project. Choose:
- the minimum and maximum viewport widths
- the base font size at each viewport
- the type scale ratio at each viewport
- the required positive and negative steps
Utopia uses these values to generate fluid clamp() declarations for every step between the minimum and maximum viewport widths.
Use step as the generated token prefix:
--step--2
--step--1
--step-0
--step-1
--step-2Copy the generated CSS into the project’s typography tokens.
Keep the generated Utopia link comment above the tokens:
/* @link https://utopia.fyi/type/calculator?... */
:root {
--step--2: ...;
--step--1: ...;
--step-0: ...;
--step-1: ...;
}The link preserves the configuration used to generate the scale. Update the tokens through the calculator rather than editing individual clamp() values manually.
The viewport sizes and scale ratios are project decisions. They should reflect the project’s content, typefaces, visual direction, and expected layout range rather than being copied unchanged between projects.
Available steps
The fluid type scale provides font sizes from --step--2 through the necessary step size.
:root {
--step--2: ...;
--step--1: ...;
--step-0: ...;
--step-1: ...;
--step-2: ...;
--step-3: ...;
--step-4: ...;
--step-5: ...;
--step-6: ...;
...
}Each token scales continuously between its minimum and maximum size as the viewport changes.
This provides responsive typography without requiring media queries for individual font sizes.
Scale direction
--step-0 is the base font size and is used for body text.
body {
font-size: var(--step-0);
}Negative steps are smaller than the base size:
--step--2
--step--1Positive steps become progressively larger:
--step-1
--step-2
--step-3
/* through the necessary step */The step number describes a position in the scale. It does not prescribe a particular HTML element or content role.
Choose a step
Choose a step according to the hierarchy required by the context.
Use smaller steps for supporting text where the content remains readable and does not require stronger prominence.
Use larger steps to establish hierarchy between headings, page titles, and display text.
Do not assign a fixed type step globally to every h1, h2, or other heading level. The required visual size depends on the surrounding interface and content hierarchy.
Heading structure
Choose heading elements according to the document structure:
<h1>Page title</h1>
<h2>Section title</h2>
<h3>Subsection title</h3>Choose their visual appearance separately:
.page-title {
font-family: var(--font-heading);
font-size: var(--step-5);
font-weight: var(--font-bold);
}
.section-title {
font-family: var(--font-heading);
font-size: var(--step-3);
font-weight: var(--font-semibold);
}Do not choose an h2 instead of an h1 because its browser default appears smaller. Preserve the heading hierarchy and style it with CSS.
Base typography
The base stylesheet establishes the default text presentation:
body {
font-family: var(--font-body);
font-size: var(--step-0);
font-weight: var(--font-regular);
line-height: 1.5;
color: var(--color-text);
font-optical-sizing: auto;
}The body defaults provide readable ordinary text while allowing components and content styles to establish their own hierarchy.
Line height
The default body line height is unitless so it scales with the active font size:
line-height: 1.5;Font-size tokens do not include a line height. Define an explicit line height when a typographic style requires something different from the inherited value.
Larger display text commonly needs a tighter line height than body text, but the correct value depends on the typeface, size, width, and content. Do not derive it from the font size token automatically.
Optical sizing
font-optical-sizing: auto;Optical sizing allows a supporting variable font to adjust its letterforms for the rendered text size.
It has no visual effect when the active font does not provide an optical-size axis.
Text wrapping
The base stylesheet allows paragraphs and headings to wrap long content rather than overflow their container:
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}This is a fallback for unusually long words and strings. It does not replace appropriate container sizing or content editing.
Usage
Use typography tokens together to define a text style:
.card-title {
font-family: var(--font-heading);
font-size: var(--step-2);
font-weight: var(--font-semibold);
line-height: 1.2;
}Use the body family for ordinary interface copy:
.card-description {
font-family: var(--font-body);
font-size: var(--step-0);
font-weight: var(--font-regular);
line-height: 1.5;
}Use the monospace family for code and technical values:
.token-name {
font-family: var(--font-mono);
font-size: var(--step--1);
}Loading custom fonts
Projects that need custom typefaces can load them through Astro’s Fonts API and assign them to the shared typography roles.
Projects that use the Foundations system-font defaults do not need to configure or load additional fonts.
The custom-font process has three layers:
- configure the font and its provider in
astro.config - add the configured font to the document head
- map the generated font variable to a typography role
Configure the font
Register each required font through the fonts option in the Astro configuration.
import { defineConfig, fontProviders } from "astro/config";
export default defineConfig({
fonts: [
{
provider: fontProviders.fontsource(),
name: "Project Heading Font",
cssVariable: "--font-project-heading",
weights: ["400", "600", "700"],
styles: ["normal"],
fallbacks: ["sans-serif"],
},
{
provider: fontProviders.fontsource(),
name: "Project Body Font",
cssVariable: "--font-project-body",
weights: ["300", "400", "500", "600", "700"],
styles: ["normal"],
fallbacks: ["sans-serif"],
},
],
});The provider, family name, variants, and fallback family depend on the selected project fonts. These settings do not change the shared --font-* role API used by components.
Astro also supports other built-in providers and local font files.
Request only the weights and styles the project uses. Loading unnecessary variants increases the number or size of font resources.
Variable fonts can be configured with their supported weight range instead of listing separate static weights.
Add the fonts to the document
Import Astro’s Font component into the shared base layout and render one instance for each configured font variable.
---
// BaseHead.astro
import { Font } from "astro:assets";
---
<!-- Charset & Viewport -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- rest of metadata, OpenGraph, Favicon, etc... -->
<!-- Fonts -->
<Font cssVariable="--font-project-heading" preload />
<Font cssVariable="--font-project-body" />A configured font is not applied merely by adding it to astro.config. The corresponding <Font /> component must be included in the document head before its CSS variable can be used.
Preload only fonts required for immediately visible content. Preloading every configured font can compete with other important page resources.
Assign font roles
Override the Foundations defaults by mapping Astro’s generated font variables to the shared typography roles:
:root {
--font-heading: var(--font-project-heading);
--font-body: var(--font-project-body);
--font-mono: var(--font-project-mono);
}Components and project styles continue using the role variables:
.page-title {
font-family: var(--font-heading);
}
body {
font-family: var(--font-body);
}
code {
font-family: var(--font-mono);
}Do not reference --font-project-heading or another provider-specific variable throughout component styles.
Keeping project font variables behind the shared role tokens allows the selected family or loading method to change without changing every consumer.
Only override the roles required by the project. Any role left unchanged continues using its Foundations default.
Font fallbacks
Foundations uses system-font stacks as its default typography roles, so text remains available without external font resources.
When configuring a custom font through Astro, define an appropriate fallback for that family.
Astro can generate an optimized fallback based on the configured fallback family. This helps reduce layout movement while the primary font loads.
Test the interface with:
- the primary fonts loaded
- the primary fonts still loading
- the primary fonts unavailable
- the Foundations system-font defaults
- the narrowest and widest type-scale values
- every font weight used by the project
Accessibility
Text must remain usable when users resize it or override its presentation.
Check that:
- text can be resized to 200% without losing content or functionality
- layouts reflow without clipping or overlapping text
- components tolerate increased line, paragraph, word, and letter spacing
- small text remains readable at its actual weight and contrast
- headings follow a logical semantic hierarchy
- essential text is not presented only as an image
- font loading failure does not hide content
Avoid fixed-height text containers unless their content and resizing behaviour are fully controlled.
Do not disable browser zoom or use text sizes that depend on users retaining the default browser settings.