Skip to content

Commit 2ffce4b

Browse files
authored
docs(design-system-docs): rewrite typography for EDS 2.0 (#4866)
* docs(design-system-docs): rewrite typography for EDS 2.0 Replace EDS 1.0 named text styles with the modular type scale, density and line-height model. Adds principles, baseline-grid alignment, the Element/Selectable/Container/Page categorisation, and a token reference. Embeds the Storybook UIText and LongFormText stories as live demos. Closes #4861 * docs(design-system-docs): correct Typography Figma usage Typography in Figma is a single component driven by variable modes on the instance, not per-layer text styles or frame-level modes. Mirrors how the matching CSS variables resolve in code. * docs(design-system-docs): clarify Typography mode cascade in Figma Cascade only applies while modes remain at default; explicit mode overrides on an instance break the inheritance. * docs(design-system-docs): add live type specimen for both typefaces Renders the modular scale at every step in both Inter and Equinor inline, using rem values from spacious density. Replaces the static 'Inter scale' and 'Equinor scale' Figma exports — the specimen stays accurate to the scale automatically and shows both typefaces together so the x-height adjustment is visible. * docs(design-system-docs): show pixel size next to each specimen Adds a px column for both Inter and Equinor so readers can see the rendered size alongside the sample, matching the original Figma layout. * docs(design-system-docs): address typography review feedback - Fix Tokens table: correct font-weight to namespaced form, rename letter-spacing to tracking, add header font-size and line-height rows - Reverse x-height explanation: Equinor has shorter x-height than Inter, not taller (verified from font OS/2 metrics: 0.480 vs 0.546) - Switch Equinor specimen fallback from serif to sans-serif - Extract repeated specimen label inline styles into .specimen-label - Add WCAG callout reserving xs for decoration - Convert :::info **Do** / :::danger **Don't** to bracketed-title syntax - Un-indent closing ::: markers so Docusaurus parses them - Drop deprecated frameborder attribute from Storybook iframes - Add note that iframe demos only show xs-3xl - Clarify density cascade reaches every typography axis - Remove Commit Mono until it is properly shipped with tokens * docs(design-system-docs): drive type specimen from tokens Replace the inline specimen grid (hardcoded fontSize values per row) with a TypeSpecimen component that resolves fontSize from --eds-typography-{ui-body,header}-{size}-font-size and reads back the computed pixel value via getComputedStyle. The label and rendered text now come from the same source — if the token scale changes, both update. * docs(design-system-docs): drop incorrect WCAG SC 1.4.4 reference in xs warning * chore(config): downgrade two type-aware rules to warn for docs app typescript-eslint cannot resolve React types when linting files under apps/design-system-docs because tsconfig.eslint.json (matched first by **/*.tsx) doesn't include React in its project graph. The codebase already downgrades no-unsafe-return / -assignment / -argument to warn globally; extend the same treatment to no-unsafe-call and no-unsafe-member-access, scoped to the docs app, so CI doesn't fail on hooks usage in docs-only React components.
1 parent 06177a2 commit 2ffce4b

4 files changed

Lines changed: 311 additions & 25 deletions

File tree

Lines changed: 189 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,228 @@
1+
import TypeSpecimen from '@site/src/components/TypeSpecimen'
2+
13
# Typography
24

3-
Typography presents hierarchy and organises information as clearly and efficiently as possible.
5+
Typography presents hierarchy and organises information as clearly and efficiently as possible. In EDS 2.0 it does more than that — type is also the **minimum spacing unit** the rest of the system is built on. Padding, gap, line-height and component height all derive from the active text size.
6+
7+
## Principles
8+
9+
EDS 2.0 typography rests on four principles. They sit underneath every decision about size, weight, line-height and alignment.
10+
11+
### To ensure safety – legibility always comes first
12+
13+
When it comes to typography, ensuring safety is our main goal, and we make sure of it through legibility. Anything that compromises a user's ability to read accurately and quickly is a regression, no matter how much it improves on other axes.
14+
15+
### We build from the inside to the outside
16+
17+
We build from our minimal unit — the text — and from there we define spatial proportions for paddings, line-heights and gaps that ensure harmony. Spacing isn't decided in isolation; it follows from the text it surrounds.
18+
19+
### Visual alignment over metric alignment
20+
21+
All our metrics are ruled by the need to create harmony. By creating harmonic visual rhythms in interfaces we make them easier to use. When the numbers and the eye disagree, the eye wins.
22+
23+
### A balance between flexibility and rigidity
24+
25+
We search for a balance of options that avoids decision paralysis but still gives teams enough freedom to express their specific needs. The system is opinionated where consistency matters, and open where context varies.
26+
27+
## Typefaces
428

5-
## Font families
29+
EDS 2.0 uses two typefaces, each with a clear job:
630

7-
EDS uses two font families:
31+
| Context | Typeface |
32+
| ---------------- | ---------------------------- |
33+
| UI labels & body | **Inter** (variable) |
34+
| Headings | **Equinor** (brand typeface) |
835

9-
- **Equinor** -- Primary typeface for headings (`data-font-family="header"`)
10-
- **Inter** -- UI/body typeface for interface text such as buttons, inputs, and labels (`data-font-family="ui"`)
36+
Inter covers all UI text — button labels, form fields, captions, prose. Equinor is reserved for display headings. Equinor is rendered slightly larger at every step than Inter — its shorter x-height needs a small upward bump so headings and surrounding UI text feel optically equivalent.
37+
38+
### Specimens
39+
40+
The same modular scale drives both typefaces. Spacious density (`lg` = 16 px) shown.
41+
42+
<TypeSpecimen />
1143

1244
### Loading fonts
1345

1446
To use EDS components correctly, you must load both font families. The recommended approach is the EDS variable font stylesheet, which includes both Equinor and Inter:
1547

1648
```html
17-
<link rel="stylesheet" href="https://cdn.eds.equinor.com/font/eds-uprights-vf.css" />
49+
<link
50+
rel="stylesheet"
51+
href="https://cdn.eds.equinor.com/font/eds-uprights-vf.css"
52+
/>
1853
```
1954

2055
:::warning eds-uprights-vf.css required for EDS 2.0 components
2156
The older `equinor-font.css` stylesheet only includes the Equinor font. If you use EDS 2.0 (`next`) components with only `equinor-font.css`, UI components like Button and TextField will fall back to a generic sans-serif because the Inter font is missing.
2257
:::
2358

24-
## Guidelines
59+
## The modular type scale
60+
61+
Too many type sizes cause confusion. EDS 2.0 uses a single modular scale with ten steps from `xs` to `6xl`, anchored at `lg`. Steps follow a geometric progression — five steps per octave (`pow(2, n/5)`) — so consecutive sizes always feel like the same kind of jump.
62+
63+
| Step | Multiplier | Spacious (lg = 16px) |
64+
| ----- | ---------------- | -------------------- |
65+
| `xs` | ×2^(−3/5) ≈ 0.66 | 0.65625rem (10.5px) |
66+
| `sm` | ×2^(−2/5) ≈ 0.76 | 0.75rem (12px) |
67+
| `md` | ×2^(−1/5) ≈ 0.87 | 0.875rem (14px) |
68+
| `lg` | ×1 | 1rem (16px) |
69+
| `xl` | ×2^(1/5) ≈ 1.15 | 1.15625rem (18.5px) |
70+
| `2xl` | ×2^(2/5) ≈ 1.32 | 1.3125rem (21px) |
71+
| `3xl` | ×2^(3/5) ≈ 1.52 | 1.53125rem (24.5px) |
72+
| `4xl` | ×2^(4/5) ≈ 1.74 | 1.75rem (28px) |
73+
| `5xl` | ×2^(5/5) = 2 | 2rem (32px) |
74+
| `6xl` | ×2^(6/5) ≈ 2.30 | 2.3125rem (37px) |
75+
76+
:::warning Reserve `xs` for decoration
77+
At spacious density `xs` is 10.5 px and at comfortable it falls to ~9 px. Use it for decorative or supplementary labels (badges, captions on dense visualisations), never for body copy or primary content.
78+
:::
79+
80+
<iframe
81+
class="sb-iframe"
82+
src="https://storybook.eds.equinor.com/iframe.html?globals=&args=&id=eds-2-0-beta-foundation-typography--ui-text"
83+
width="100%"
84+
height="520"
85+
></iframe>
86+
87+
[View in Storybook](https://storybook.eds.equinor.com/?path=/story/eds-2-0-beta-foundation-typography--ui-text)
2588

26-
The Equinor typeface is the primary typeface and is available in four weights: `light`, `regular`, `medium` and `bold` with accompanying italics. Please do not use `light` in digital interfaces except in special cases where the font size is over 48px.
89+
_Demo shows `xs``3xl`; the full scale extends to `6xl`._
2790

28-
:::info Note
29-
Typography is called text styles in Figma.
91+
## Density
92+
93+
The whole scale shifts when density changes — only the `--_base` value moves; every derived size and line-height re-derives automatically.
94+
95+
| Density | Base |
96+
| ------------------ | ----- |
97+
| Spacious (default) | 16 px |
98+
| Comfortable | 14 px |
99+
100+
Density is applied by setting `data-density` on an ancestor element. Components within that ancestor then read the cascaded base. The cascade reaches every typography axis (`font-size`, `line-height`, `tracking`, `font-weight`), so a single `data-density` is enough — you don't need to set `data-font-size` or other axes on individual components.
101+
102+
```html
103+
<div data-density="comfortable">
104+
<!-- everything here uses the comfortable scale -->
105+
</div>
106+
```
107+
108+
:::info Accessibility
109+
When you opt into `comfortable`, give users a way to switch back to `spacious`. Smaller default text shouldn't be a one-way door.
30110
:::
31111

112+
## Line-height: default vs. squished
113+
114+
Every size step has two line-height variants. Pick the one that matches what the text is doing, not the size it happens to be.
115+
116+
- **`squished`** — tight leading for UI controls. Use this wherever text sits inside a component (buttons, inputs, badges, tabs). The reduced line-height keeps controls compact without sacrificing single-line legibility.
117+
- **`default`** — comfortable leading for reading. Use this for body copy, descriptions, tooltips, and any text that may wrap to multiple lines.
118+
119+
<iframe
120+
class="sb-iframe"
121+
src="https://storybook.eds.equinor.com/iframe.html?globals=&args=&id=eds-2-0-beta-foundation-typography--long-form-text"
122+
width="100%"
123+
height="640"
124+
></iframe>
125+
126+
[View in Storybook](https://storybook.eds.equinor.com/?path=/story/eds-2-0-beta-foundation-typography--long-form-text)
127+
128+
_Demo shows `xs``3xl`; the full scale extends to `6xl`._
129+
130+
<details>
131+
<summary>Line-height multipliers per step</summary>
132+
133+
The multiplier decreases as font size increases, following an ease-out cubic curve — `1.39 − pow(n/9, 3) × 0.29` for default and `1.13 − pow(n/9, 3) × 0.13` for squished, where `n` is the step index (0 = `xs`, 9 = `6xl`). Reduction is barely perceptible at small sizes and accelerates toward the top of the scale, keeping large display text optically tight without compressing body copy.
134+
135+
| Step | Default ≈ | Squished ≈ |
136+
| ----- | --------- | ---------- |
137+
| `xs` | 1.39 | 1.13 |
138+
| `sm` | 1.39 | 1.13 |
139+
| `md` | 1.39 | 1.13 |
140+
| `lg` | 1.38 | 1.13 |
141+
| `xl` | 1.36 | 1.12 |
142+
| `2xl` | 1.34 | 1.11 |
143+
| `3xl` | 1.30 | 1.09 |
144+
| `4xl` | 1.25 | 1.07 |
145+
| `5xl` | 1.19 | 1.04 |
146+
| `6xl` | 1.10 | 1.00 |
147+
148+
</details>
149+
32150
### Paragraph length
33151

34-
Paragraph length is the number of characters in a line of text---this includes spaces. To ensure readability, line length should be 55-80 characters. Lines less than 55 characters can cause strain on the eye requiring the eye to jump to the next line too quickly, breaking the reading rhythm. Lines greater than 80 characters can make it difficult for users to continue on the correct line in a large body of text.
152+
For prose, line length should sit between **55–80 characters** including spaces. Shorter lines force the eye to jump too often, breaking the reading rhythm; longer lines make it hard to find the next line in a large block of text.
35153

36-
## Styles
154+
## Baseline-grid alignment
37155

38-
Too many type sizes can cause confusion. The EDS has a limited set of type sizes that work well together.
156+
EDS 2.0 treats single-line and multi-line text differently.
39157

40-
### Headings
158+
**Single-line UI text is _not_ snapped to the baseline grid.** The component derives its height from cap-height (`1cap`) rounded to the nearest 4 px, then distributes the remaining line-height symmetrically as padding. This keeps element height predictable and optically centred regardless of font.
41159

42-
There are seven headings to choose between: `H1bold`, `H1`, `H2`, `H3`, `H4`, `H5` and `H6`. Sentence case (The quick brown fox…) should be used instead of title case (The Quick Brown Fox…) since it contributes to better readability.
160+
**Multi-line text uses baseline trimming** (`text-box: trim-both ex alphabetic`). The top of the text block aligns to the baseline grid, so wrapping content sits consistently alongside other elements.
43161

44-
### Paragraph
162+
:::danger Anti-pattern: multi-line button labels
163+
Multi-line button labels are harder to scan and weaker as call-to-action elements. Shorten the label instead. The `multiline` prop on Button is a safety valve, not a design choice.
164+
:::
45165

46-
There are many paragraph styles to choose between: `Overline`, `Ingress`, `Body long`, `Body long link`, `Body long italic`, `Body long bold`, `Body long bold italic`, `Body short`, `Body short link`, `Body short italic`, `Body short bold`, `Body short bold italic` and `Caption`.
166+
## Categorisation: where text lives
47167

48-
The style `Body short` is used for short sentences containing around four words. It is most commonly used within components.
168+
The system categorises every piece of UI by what surrounds the text. The category controls how the text is aligned vertically.
49169

50-
### Additional styles
170+
- **Elements** — Headings and body copy. Aligned to the baseline grid.
171+
- **Selectables** — Anything clickable that contains a single line of text: chips, buttons, autocomplete, search, text fields, menu items, list items, labels, breadcrumbs, pagination. **Vertically centred** to the UI control.
172+
- **Containers** — Popovers, tables, cards, side sheets, accordions, banners, dialogs and similar surfaces. Aligned to the baseline grid.
173+
- **Pages** — Top-level groups of containers (the main content area excluding top bar and sidebar). Aligned to the baseline grid.
51174

52-
There are other typography styles that are used internally within components.
175+
Knowing the category up-front tells you which line-height variant to pick and which alignment behaviour to expect.
176+
177+
## Tokens
178+
179+
| Concept | Token |
180+
| ----------- | ------------------------------------------------------- |
181+
| Font family | `--eds-typography-header-font-family` |
182+
| | `--eds-typography-ui-body-font-family` |
183+
| Font size | `--eds-typography-ui-body-{size}-font-size` |
184+
| | `--eds-typography-header-{size}-font-size` |
185+
| Font weight | `--eds-typography-{family}-{size}-font-weight-lighter` |
186+
| | `--eds-typography-{family}-{size}-font-weight-normal` |
187+
| | `--eds-typography-{family}-{size}-font-weight-bolder` |
188+
| Line height | `--eds-typography-{family}-{size}-line-height-default` |
189+
| | `--eds-typography-{family}-{size}-line-height-squished` |
190+
| Tracking | `--eds-typography-{family}-{size}-tracking-tight` |
191+
| | `--eds-typography-{family}-{size}-tracking-normal` |
192+
| | `--eds-typography-{family}-{size}-tracking-wide` |
193+
194+
`{family}` is one of `ui-body`, `header`. `{size}` is one of `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`.
53195

54196
## Implementation in Figma
55197

56-
### How to add
198+
EDS 2.0 doesn't ship a per-layer text-style library. Instead, all text is rendered through a single **Typography component** whose appearance is driven by variable modes set on the instance itself. The same `--eds-typography-*` variables that resolve in code (`font-family`, `font-size`, `font-weight`, `line-height`, `tracking`) resolve in Figma — switching a mode swaps the values they point to.
199+
200+
To set typography in Figma:
201+
202+
1. From the **Assets** panel, drag in an **EDS Typography** instance.
203+
2. Type the content into the `text` property.
204+
3. With the instance selected, open the **Variables** section in the **Design** panel and pick the mode for **size**, **weight**, **family**, **line-height** and **density**. The instance updates immediately.
205+
4. Nest the instance inside whatever container needs it — buttons, list items, page headings — without flattening or detaching.
206+
207+
A freshly dragged-in instance leaves every mode at its default, so it inherits whatever modes are set on its parents. Set the density mode once on a page-level frame and every default Typography instance inside follows. The cascade stops at any instance where you've explicitly set a mode — that override sticks regardless of the parent.
57208

58-
1. Locate the _layer_ in the **Layers Panel** that needs typography style applied.
209+
## Do's and Don'ts
59210

60-
2. Locate the **Design** tab in the **Inspector Panel**.
211+
:::info[Do]
61212

62-
3. Under the **Text** section, open the **Style library** menu to view the text styles in order of relevance.
213+
- Pick the line-height variant by what the text is doing — `squished` for controls, `default` for prose
214+
- Use sentence case (capitalise only the first word) for headings and labels
215+
- Keep paragraph lines between 55–80 characters
216+
- Treat density as a global setting on an ancestor — don't switch it per component
217+
- Provide a way to switch back to spacious when you opt into comfortable density
63218

64-
4. Choose a style by clicking on the style needed.
219+
:::
220+
221+
:::danger[Don't]
222+
223+
- Wrap button or chip labels over multiple lines — shorten the label instead
224+
- Mix typefaces within a single piece of UI; let Inter do the UI work and Equinor do the headlines
225+
- Rely on font weight alone to communicate hierarchy without supporting size or position
226+
- Override `font-size` directly in components — set density on a parent and let the scale re-derive
227+
228+
:::
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React, { Fragment, useEffect, useRef, useState } from 'react'
2+
3+
const SIZES = [
4+
'6xl',
5+
'5xl',
6+
'4xl',
7+
'3xl',
8+
'2xl',
9+
'xl',
10+
'lg',
11+
'md',
12+
'sm',
13+
'xs',
14+
] as const
15+
16+
type Size = (typeof SIZES)[number]
17+
type Family = 'ui-body' | 'header'
18+
19+
const FAMILY_LABEL = {
20+
'ui-body': 'Inter — body / UI',
21+
header: 'Equinor — headings',
22+
} as const
23+
24+
const FAMILY_FONT = {
25+
'ui-body': 'Inter, sans-serif',
26+
header: 'Equinor, sans-serif',
27+
} as const
28+
29+
const SAMPLE = {
30+
'ui-body': "I'm body text",
31+
header: "I'm a heading",
32+
} as const
33+
34+
function formatPx(px: number): string {
35+
return `${Number.isInteger(px) ? px : px.toFixed(1)}px`
36+
}
37+
38+
function Specimen({ size, family }: { size: Size; family: Family }) {
39+
const ref = useRef<HTMLSpanElement>(null)
40+
const [px, setPx] = useState<string>('')
41+
42+
useEffect(() => {
43+
if (!ref.current) return
44+
setPx(formatPx(parseFloat(getComputedStyle(ref.current).fontSize)))
45+
}, [size, family])
46+
47+
return (
48+
<>
49+
<code className="specimen-label">{px}</code>
50+
<span
51+
ref={ref}
52+
style={{
53+
fontSize: `var(--eds-typography-${family}-${size}-font-size)`,
54+
fontFamily: FAMILY_FONT[family],
55+
whiteSpace: 'nowrap',
56+
}}
57+
>
58+
{SAMPLE[family]}
59+
</span>
60+
</>
61+
)
62+
}
63+
64+
export function TypeSpecimen() {
65+
return (
66+
<div
67+
style={{
68+
display: 'grid',
69+
gridTemplateColumns: '3rem auto 1fr auto 1fr',
70+
columnGap: '1rem',
71+
rowGap: '0.75rem',
72+
alignItems: 'baseline',
73+
padding: '1.5rem',
74+
border: '1px solid var(--ifm-color-emphasis-300)',
75+
borderRadius: '8px',
76+
margin: '1rem 0',
77+
overflowX: 'auto',
78+
}}
79+
>
80+
<code className="specimen-label">step</code>
81+
<code className="specimen-label" />
82+
<span
83+
className="specimen-label"
84+
style={{ fontFamily: 'Inter, sans-serif' }}
85+
>
86+
{FAMILY_LABEL['ui-body']}
87+
</span>
88+
<code className="specimen-label" />
89+
<span
90+
className="specimen-label"
91+
style={{ fontFamily: 'Inter, sans-serif' }}
92+
>
93+
{FAMILY_LABEL.header}
94+
</span>
95+
96+
{SIZES.map((size) => (
97+
<Fragment key={size}>
98+
<code className="specimen-label">{size}</code>
99+
<Specimen size={size} family="ui-body" />
100+
<Specimen size={size} family="header" />
101+
</Fragment>
102+
))}
103+
</div>
104+
)
105+
}
106+
107+
export default TypeSpecimen

apps/design-system-docs/src/css/custom.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ nav.menu li a {
234234
background-color: var(--eds-color-bg-neutral-surface);
235235
}
236236

237+
/* ======================================
238+
* TYPOGRAPHY SPECIMEN GRID
239+
* ====================================== */
240+
241+
.specimen-label {
242+
font-size: 0.75rem;
243+
opacity: 0.6;
244+
}
245+
237246
/* ======================================
238247
* TABLE OF CONTENTS
239248
* ====================================== */

eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ export default tseslint.config(
247247
{ ignore: ['^@theme', '^@docusaurus', '^@site'] },
248248
],
249249
'import/no-default-export': 'off',
250+
// The docs app isn't part of tsconfig.eslint.json's project graph, so
251+
// typescript-eslint can't resolve React types here. Downgrade these
252+
// type-aware rules to warn — matches how no-unsafe-return / -assignment
253+
// / -argument are handled globally above.
254+
'@typescript-eslint/no-unsafe-call': 'warn',
255+
'@typescript-eslint/no-unsafe-member-access': 'warn',
250256
},
251257
},
252258

0 commit comments

Comments
 (0)