Typography
Typography sets how Nuton text looks: Fraunces for titles, Inter for everything else, and a fixed set of type styles. Use a type style for every piece of text instead of setting sizes by hand.
Families, font sizes and all type styles come from Figma. The fallback stacks, the two weights and the Fraunces axes (D6) are code-only; there are no interim typography tokens. Beta until the Figma text styles are bound to the size variables (TD-12) and the duplicate Subheading/2X Small style name is cleaned up (D9).
Overview
Two families, four groups of styles. Titles use Fraunces and carry the brand voice; labels, paragraphs and subheadings use Inter, which stays readable at small sizes in dense UI.
| Group | Family | Use it for |
|---|---|---|
| Title (H1–H6) | Fraunces | Page and section titles, lesson and space names, empty-state headlines. |
| Label | Inter, medium | Single-line UI text: buttons, field labels, menu items, tabs, stats. |
| Paragraph | Inter, regular | Running text that can wrap: descriptions, hints, lesson content. |
| Subheading | Inter, medium | Small overlines and group headings above lists, such as a course name over a lesson title. |
Biology 101 · Week 3
Photosynthesis, explained
Plants turn light, water and carbon dioxide into sugar. This lesson walks through each stage with a short quiz at the end.
12 min · 3 sections
// typeStyle() is the helper from Usage
<p style={typeStyle('subheading-x-small')}>Biology 101 · Week 3</p>
<h3 style={typeStyle('title-h4')}>Photosynthesis, explained</h3>
<p style={typeStyle('paragraph-medium')}>
Plants turn light, water and carbon dioxide into sugar. This lesson walks
through each stage with a short quiz at the end.
</p>
<p style={typeStyle('label-small')}>12 min · 3 sections</p>Tokens
Every value below is read from @nuton/tokens when this page builds. Primitive font tokens (families, sizes, weights) feed composite type styles; components and product code use the type styles.
Title styles
Photosynthesis, explained
Photosynthesis, explained
Photosynthesis, explained
Photosynthesis, explained
Photosynthesis, explained
Photosynthesis, explained
Label styles
Start lesson
Start lesson
Start lesson
Start lesson
Start lesson
Paragraph styles
Plants turn light into chemical energy they can store.
Plants turn light into chemical energy they can store.
Plants turn light into chemical energy they can store.
Plants turn light into chemical energy they can store.
Plants turn light into chemical energy they can store.
Subheading styles
Biology 101 · Week 3
Biology 101 · Week 3
Biology 101 · Week 3
Biology 101 · Week 3
Each style is five tokens, --nu-type-<style>-family, -size, -line-height, -tracking and -weight, generated from the Figma text style of the same name (Title/H1 Title becomes title-h1). Figma has two styles named Subheading/2X Small; the build tells them apart by size, so the 12px one is subheading-x-small(D9 cleanup).
Anatomy of a type style
| Token | Value | Source |
|---|---|---|
--nu-type-label-small-family | → font-body"Inter", var(--nu-font-fallback-body) | FigmaTextStyle/Label/Small |
--nu-type-label-small-size | → font-size-label-small14px | FigmaTextStyle/Label/Small |
--nu-type-label-small-line-height | 20px | FigmaTextStyle/Label/Small |
--nu-type-label-small-tracking | -0.006em | FigmaTextStyle/Label/Small |
--nu-type-label-small-weight | 500 | FigmaTextStyle/Label/Small |
Family and size are aliases of the primitive font tokens. Line height, tracking and weight are read from the text style itself.
Families and fallbacks
| Token | Value | Source |
|---|---|---|
--nu-font-header | "Fraunces", var(--nu-font-fallback-header) | FigmaTypography/Header |
--nu-font-body | "Inter", var(--nu-font-fallback-body) | FigmaTypography/Body |
--nu-font-fallback-header | Georgia, "Times New Roman", serif | Code-only |
--nu-font-fallback-body | -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif | Code-only |
Figma variables hold only the family name. The fallback stacks are code-only, so text stays readable before the web font loads or if it fails.
Fraunces axes (D6)
| Token | Value | Source |
|---|---|---|
--nu-font-variation-display | "SOFT" 0, "WONK" 0 | Code-only |
D6 (approved): Nuton products use Fraunces with WONK set to 0, the calm, non-wonky letterforms. The marketing site currently uses WONK 1; D6 sets 0 for the system. Apply this token wherever Fraunces is used; the title styles above already do.
Weights
| Token | Value | Source |
|---|---|---|
--nu-font-weight-regular | 400 | Code-only |
--nu-font-weight-medium | 500 | Code-only |
Code-only, derived from the Figma text style names (Regular, Medium). These are the only two weights in the system.
Font sizes
| Token | Value | Source |
|---|---|---|
--nu-font-size-title-h1 | 56px | FigmaTypography/Title/H1 |
--nu-font-size-title-h2 | 48px | FigmaTypography/Title/H2 |
--nu-font-size-title-h3 | 40px | FigmaTypography/Title/H3 |
--nu-font-size-title-h4 | 32px | FigmaTypography/Title/H4 |
--nu-font-size-title-h5 | 24px | FigmaTypography/Title/H5 |
--nu-font-size-title-h6 | 20px | FigmaTypography/Title/H6 |
--nu-font-size-label-x-large | 24px | FigmaTypography/Label/X large |
--nu-font-size-label-large | 18px | FigmaTypography/Label/Large |
--nu-font-size-label-medium | 16px | FigmaTypography/Label/Medium |
--nu-font-size-label-small | 14px | FigmaTypography/Label/Small |
--nu-font-size-label-x-small | 12px | FigmaTypography/Label/X Small |
--nu-font-size-paragraph-x-large | 24px | FigmaTypography/Paragraph/X large |
--nu-font-size-paragraph-large | 18px | FigmaTypography/Paragraph/Large |
--nu-font-size-paragraph-medium | 16px | FigmaTypography/Paragraph/Medium |
--nu-font-size-paragraph-small | 14px | FigmaTypography/Paragraph/Small |
--nu-font-size-paragraph-x-small | 12px | FigmaTypography/Paragraph/X Small |
--nu-font-size-subheading-medium | 16px | FigmaTypography/Subheading/Medium |
--nu-font-size-subheading-small | 14px | FigmaTypography/Subheading/Small |
--nu-font-size-subheading-x-small | 12px | FigmaTypography/Subheading/X Small |
--nu-font-size-subheading-2x-small | 11px | FigmaTypography/Subheading/2X Small |
Figma size variables aren't bound to the text styles (TD-12)
The Typography/* size variables exist in Figma, but the text styles don't use them; each style has its own size. The build links them by name, so today they agree. Until the DS Owner binds the styles to the variables, a size changed in one place won't follow in the other. Output stays in px (TD-12).
Usage
1. Load the fonts and map the tokens
@nuton/tokens names the families but doesn't ship font files. Load Fraunces (with its SOFT and WONKaxes) and Inter yourself, then point --nu-font-header and --nu-font-body at them. Keep the fallback token at the end of each stack.
// app/layout.tsx
import { Fraunces, Inter } from 'next/font/google'
import '@nuton/tokens/tokens.css'
const fraunces = Fraunces({ subsets: ['latin'], axes: ['SOFT', 'WONK', 'opsz'], variable: '--font-fraunces', display: 'swap' })
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${fraunces.variable} ${inter.variable}`}>
{/* next/font generates hashed family names; point the Nuton font tokens at them. */}
<body style={{
'--nu-font-header': 'var(--font-fraunces), var(--nu-font-fallback-header)',
'--nu-font-body': 'var(--font-inter), var(--nu-font-fallback-body)',
} as React.CSSProperties}>
{children}
</body>
</html>
)
}/* If you load the fonts under their real family names ("Fraunces", "Inter"),
for example with @font-face or a font service, the default tokens already
point at them and no mapping is needed. Only remap when the names differ: */
:root {
--nu-font-header: "Fraunces Variable", var(--nu-font-fallback-header);
--nu-font-body: "Inter Variable", var(--nu-font-fallback-body);
}2. Apply a type style
.lesson-title {
font-family: var(--nu-type-title-h4-family);
font-size: var(--nu-type-title-h4-size);
line-height: var(--nu-type-title-h4-line-height);
letter-spacing: var(--nu-type-title-h4-tracking);
font-weight: var(--nu-type-title-h4-weight);
font-variation-settings: var(--nu-font-variation-display); /* D6, Fraunces only */
}
.lesson-summary {
font-family: var(--nu-type-paragraph-small-family);
font-size: var(--nu-type-paragraph-small-size);
line-height: var(--nu-type-paragraph-small-line-height);
letter-spacing: var(--nu-type-paragraph-small-tracking);
font-weight: var(--nu-type-paragraph-small-weight);
color: var(--nu-text-sub-600);
}/* app.css */
@import "tailwindcss";
@import "@nuton/tokens/tokens.css";
@import "@nuton/tokens/tailwind-v4-theme.css";
// Each text-* utility sets size, line height, tracking and weight.
// The family is separate: font-header or font-body.
<h2 className="font-header text-title-h4 [font-variation-settings:var(--nu-font-variation-display)]">
Photosynthesis, explained
</h2>
<p className="font-body text-paragraph-small text-text-sub-600">
A 12-minute lesson with a short quiz at the end.
</p>// tailwind.config.js
module.exports = { presets: [require('@nuton/tokens/tailwind-v3-preset')] }
// fontSize entries carry line height, tracking and weight; fontFamily is header / body.
<h2 className="font-header text-title-h4 [font-variation-settings:var(--nu-font-variation-display)]">
Photosynthesis, explained
</h2>
<p className="font-body text-paragraph-small text-text-sub-600">
A 12-minute lesson with a short quiz at the end.
</p>import type { CSSProperties } from 'react'
/** Applies a Nuton type style from its tokens. Fraunces styles also get the D6 axes. */
export function typeStyle(style: string, header = style.startsWith('title-')): CSSProperties {
return {
fontFamily: `var(--nu-type-${style}-family)`,
fontSize: `var(--nu-type-${style}-size)`,
lineHeight: `var(--nu-type-${style}-line-height)`,
letterSpacing: `var(--nu-type-${style}-tracking)`,
fontWeight: `var(--nu-type-${style}-weight)` as CSSProperties['fontWeight'],
fontVariationSettings: header ? 'var(--nu-font-variation-display)' : undefined,
}
}Tailwind names available: font-header, font-body and text-<style> for all twenty styles (text-title-h1 … text-subheading-2x-small). There are no weight, tracking or line-height utilities of their own, and no utility for the Fraunces axes, so use the arbitrary property shown.
Choosing a style
- Use the composite
--nu-type-*tokens, not the primitive--nu-font-size-*ones. Size alone loses the line height and tracking that go with it. - Pick the title level by importance on the page, and the HTML heading level by document structure. They don't have to match.
- Label is for one line of UI text; Paragraph is for text that wraps. Paragraph styles have taller line heights at the same size.
- Set colour with the text colour tokens (
--nu-text-strong-950,--nu-text-sub-600), never with weight or size alone. - Use only the two weights. If something needs more emphasis, step up a style instead of adding a bolder weight.
In use
Nuton components set their own type styles, so you only style the text around them.
Lesson card
Biology 101 · Lesson 4
Cell structure and function
Compare plant and animal cells, then label each organelle in a short quiz.
- 8 of 12 students finished
- Average score 84%
<p style={typeStyle('subheading-x-small')}>Biology 101 · Lesson 4</p>
<h3 style={typeStyle('title-h5')}>Cell structure and function</h3>
<p style={typeStyle('paragraph-small')}>
Compare plant and animal cells, then label each organelle in a short quiz.
</p>
<ul>
<li><ChartLegend color="blue">8 of 12 students finished</ChartLegend></li>
<li><ChartLegend color="orange">Average score 84%</ChartLegend></li>
</ul>
<Button size="sm" leadingIcon={<RiPlayLine />}>Resume lesson</Button>Button and ChartLegend bring their own styles (Label / Small and Label / X Small). Only the overline, title and summary are styled here.
Form text
Create a space
Spaces group the lessons you teach to one class.
For example, Biology 101.
<h3 style={typeStyle('label-large')}>Create a space</h3>
<p style={typeStyle('paragraph-small')}>Spaces group the lessons you teach to one class.</p>
<Field label="Space name" hint="For example, Biology 101." required>
<Input size="sm" defaultValue="Year 9 biology" />
</Field>Field sets its label in Label / Small and its hint in Paragraph / X Small. Match the surrounding copy to them rather than restyling the field.
Try a style
Pick a style and type your own copy. The sample and its CSS are built from the tokens.
Photosynthesis turns light into chemical energy that the plant can store.
Paragraph / Medium: 16px / 28px · weight 400 · tracking -0.011em · Inter
.sample {
font-family: var(--nu-type-paragraph-medium-family);
font-size: var(--nu-type-paragraph-medium-size);
line-height: var(--nu-type-paragraph-medium-line-height);
letter-spacing: var(--nu-type-paragraph-medium-tracking);
font-weight: var(--nu-type-paragraph-medium-weight);
}Accessibility
Text resizing
Sizes and line heights are emitted in px (TD-12). They scale with browser zoom, which is how WCAG 1.4.4 (200%) is met, but they don't follow a changed default font size in browser settings. Test every screen at 200% zoom.
Never give a text container a fixed height. Let labels and buttons wrap, so zoomed or translated text isn't clipped.
Line length
Keep running text to about 45–75 characters per line, for example with max-inline-size: 65ch on the text block. There's no measure token yet; this is a layout rule, not a type style.
At 320px wide (WCAG 1.4.10, reflow) text must wrap without horizontal scrolling. Large titles may need a smaller style on narrow screens.
Contrast
From the tokens contrast check: --nu-text-strong-950 on white 17.93:1 and --nu-text-sub-600 on white 6.69:1 both pass. --nu-text-soft-400 is 2.49:1 and must not be used for text people need to read (C1, D7).
Line height and text spacing
WCAG 1.4.8 (AAA) suggests a line height of at least 1.5 for paragraphs. Ratios below are computed from the tokens. Layouts must also survive the 1.4.12 overrides (line height 1.5, letter spacing 0.12em, word spacing 0.16em) without clipping or overlap.
| Style | Size | Line height | Ratio | Paragraph use |
|---|---|---|---|---|
paragraph-x-large | 24px | 34px | 1.42 | Short text only |
paragraph-large | 18px | 30px | 1.67 | Meets 1.5 |
paragraph-medium | 16px | 28px | 1.75 | Meets 1.5 |
paragraph-small | 14px | 24px | 1.71 | Meets 1.5 |
paragraph-x-small | 12px | 16px | 1.33 | Short text only |
label-x-large | 24px | 34px | 1.42 | Single line only |
label-large | 18px | 30px | 1.67 | Single line only |
label-medium | 16px | 28px | 1.75 | Single line only |
label-small | 14px | 20px | 1.43 | Single line only |
label-x-small | 12px | 20px | 1.67 | Single line only |
Headings and semantics
- A type style is visual only. Use real
h1–h6elements in a logical order, then style them; screen-reader users navigate by heading level. - Don't make text look like a heading without the element, and don't pick a heading element just for its size.
- Subheading styles are small. Don't use them for the only label of a control or for body text.
Guidelines and related
Use the type styles for
- Every piece of text you write outside a Nuton component.
- Fraunces titles for page, space and lesson names.
- Inter labels and paragraphs for all UI and running text.
Don't
- Set font sizes, line heights or weights by hand.
- Use Fraunces for paragraphs, labels or controls.
- Restyle the text inside components; choose a component variant instead.
Do and don't
Cell structure and function
Compare plant and animal cells, then label each organelle.
Compare plant and animal cells, then label each organelle in a short quiz. Each cell type has its own structures to find.
Your space has 3 lessons waiting for review. Open each one to approve it or send it back with comments.
Your space has 3 lessons waiting for review. Open each one to approve it or send it back with comments.
Last edited 2 days ago
Last edited 2 days ago