Skip to content

Commit db63be0

Browse files
committed
Add typography guide.
1 parent eeafa7d commit db63be0

1 file changed

Lines changed: 242 additions & 19 deletions

File tree

Lines changed: 242 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Typography
3-
description: Use semantic HTML and PureStack's shared type scale for readable interfaces.
3+
description: Structure pages with real headings, use the theme's type scale and text roles, and customize fonts, sizes, and weights.
44
template: doc
55
layout:
66
showNav: true
@@ -13,10 +13,14 @@ nav:
1313

1414
# Typography
1515

16-
Good type starts with structure. Use one page title, follow it with sections in heading order, and let paragraphs and lists carry the detail. The doc template builds its page outline from `h2` and `h3` headings.
16+
In PureStack, every piece of text gets its type from the active skin. The skin defines one font family, a scale of named sizes, and a set of weights. Markdown headings, components, utility classes, and your own styles all use those same values, so changing one size updates it everywhere.
17+
18+
You choose the sizes. PureStack works out the line height and letter spacing for each size and weight, so large headings set tightly and small labels stay readable.
1719

1820
## Write a clear hierarchy
1921

22+
Start with structure. Use one `h1` per page, then `h2` for sections and `h3` for subsections, without skipping levels:
23+
2024
```mdx
2125
# Project setup
2226

@@ -31,36 +35,255 @@ The first step and its context.
3135
What the reader should see next.
3236
```
3337

34-
The heading level describes the content hierarchy. Choose its visual treatment through the theme rather than skipping levels to get a larger font.
38+
The doc template builds the "On this page" outline from `h2` and `h3` headings, so a missing level also leaves a gap in the outline. Choose a heading level for its place in the content, not for its size. You can [change the visual treatment](#utility-classes) separately.
39+
40+
Inside a doc page, Markdown elements use the scale like this:
41+
42+
| Element | Size | Weight |
43+
| --- | --- | --- |
44+
| Paragraphs, lists, tables | `body` | Regular |
45+
| `#` (`h1`) | `h1` | `w700` |
46+
| `##` (`h2`) | `h2` | `w700` |
47+
| `###` to `######` (`h3`–`h6`) | `h3` to `h6` | `w600` |
48+
| Inline code and code blocks | `xs` | Regular, monospace |
49+
50+
Headings get space above them only when they follow other content, so the first heading on a page sits flush with the top.
51+
52+
## The type scale
53+
54+
A skin defines twelve named sizes. The names describe a role, not a fixed size. `fs-h3` is the size of an `h3` in the active skin, whatever that is:
55+
56+
<div class="tone--neutral tone-fill-surface tone-border-surface b-1 rounded-md p-4">
57+
<div class="fs-display fw-700 lh-1">Display</div>
58+
<div class="fs-h1 fw-700 lh-1 mt-2">Heading 1</div>
59+
<div class="fs-h2 fw-700 lh-2 mt-2">Heading 2</div>
60+
<div class="fs-h3 fw-600 lh-3 mt-2">Heading 3</div>
61+
<div class="fs-h4 fw-600 mt-2">Heading 4</div>
62+
<div class="fs-body mt-2">Body text carries most of the page.</div>
63+
<div class="fs-sm text-subtle mt-2 mb-0">Small text for captions and supporting detail.</div>
64+
<div class="fs-xs text-subtle mt-1 mb-0">Extra small text for labels and metadata.</div>
65+
</div>
66+
67+
Sizes are `rem` values, so they scale with the [root font size](#change-the-root-size). Compare the standard skin that comes with PureStack and the custom Studio skin that this site uses:
68+
69+
| Name | Typical use | Standard skin | Studio skin (this site) |
70+
| --- | --- | --- | --- |
71+
| `display` | Hero and marketing headlines | `2.545rem` | `5rem` |
72+
| `h1` | Page title | `2rem` | `2.6875rem` |
73+
| `h2` | Section heading | `1.5rem` | `2.125rem` |
74+
| `h3` | Subsection heading | `1.25rem` | `1.5rem` |
75+
| `h4` | Minor heading, lead text | `1rem` | `1.1875rem` |
76+
| `h5` | Minor heading, quotes | `1rem` | `1.0625rem` |
77+
| `h6` | Smallest heading | `1rem` | `1rem` |
78+
| `body` | Paragraphs and controls | `1rem` | `0.875rem` |
79+
| `sm` | Captions and supporting lines | `0.94rem` | `0.75rem` |
80+
| `xs` | Labels, code | `0.9rem` | `0.6875rem` |
81+
| `xxs` | Metadata and small caps | `0.85rem` | `0.625rem` |
82+
| `xxxs` | Badges and counters | `0.72rem` | `0.5625rem` |
83+
84+
In the standard skin, `h4`, `h5`, and `h6` are the same size as body text and are told apart only by weight. If your content uses those levels often, give them distinct sizes in your skin.
85+
86+
### Weights
87+
88+
The scale has five named weights: `w100`, `w400`, `w500`, `w600`, and `w700`. Components and Markdown headings use the names, not raw numbers, so a skin can, for example, make all bold text `650` by changing `w700`. The weights only render if the font provides them. A font without a `600` file displays the nearest weight it has.
89+
90+
## Line height and letter spacing are derived
91+
92+
A skin defines sizes and weights only. When PureStack styles text, `palette.applyFont(size, weight)` returns the size and weight together with a matching line height and letter spacing:
93+
94+
| Size | Weight | Line height | Letter spacing |
95+
| --- | --- | --- | --- |
96+
| `0.75rem` | 400 | 1.6 | `0.06em` |
97+
| `0.875rem` | 400 | 1.6 | `0.03em` |
98+
| `1rem` | 400 | 1.5 | normal |
99+
| `1.25rem` | 600 | 1.3 | `-0.003em` |
100+
| `1.5rem` | 700 | 1.3 | `-0.005em` |
101+
| `2rem` | 700 | 1.1 | `-0.01em` |
102+
| `3rem` | 700 | 1.02 | `-0.02em` |
35103

36-
## Use the type scale
104+
The rules follow ordinary typesetting practice:
37105

38-
PureStack's theme palette defines a base font family, named sizes from `xxxs` through `display`, and weights. Generated utility classes expose the size scale when a piece of UI needs an explicit treatment:
106+
- **Line height tightens as size grows.** It is 1.6 for small text, 1.5 for body text, and close to 1 for display text.
107+
- **Weight tightens it further.** Weight 500 and above reduces the line height slightly, down to a floor that keeps lines from touching.
108+
- **Letter spacing opens up small text and closes up large text.** Spacing is wider below `1rem`, normal at `1rem`, and slightly negative above it, up to `-0.02em` at `3rem`.
39109

40-
```html
41-
<p class="fs-sm">A compact caption</p>
42-
<p class="fs-body">Supporting text at the body size</p>
43-
<p class="fs-h3">A prominent metric label</p>
110+
Because these values are calculated from `rem`, every size in a skin must be a `rem` string such as `"1.125rem"`. A `px` or `em` size stops the build with `fontSize must be a positive rem string`.
111+
112+
## Utility classes
113+
114+
Use utility classes when a single element needs a particular look, for example a large metric value or a small label. The [Utility CSS classes guide](/guides/utility-css-classes/#typography-and-text) lists them all.
115+
116+
- `fs-xxxs` through `fs-display` set the font size from the scale.
117+
- `fw-100` through `fw-900` set the weight.
118+
- `lh-0` through `lh-7` set the line height from `1.0` to `1.7`.
119+
120+
`fs-*` classes change the **size only**. They do not apply the derived line height, so large text keeps the line height of its surroundings. For multiline text at a large size, add a matching `lh-*` class, as the specimen above does:
121+
122+
```mdx
123+
<p class="fs-h2 fw-700 lh-1">A large statement that may wrap onto a second line</p>
44124
```
45125

46-
Keep headings as real heading elements even when their size is customized. The Studio skin uses a compact body scale; another skin can use the same names with different values.
126+
### Text roles
127+
128+
Text roles combine size, weight, letter spacing, color, and margin into one class for common editorial patterns:
129+
130+
<div class="tone--neutral tone-fill-surface tone-border-surface b-1 rounded-md p-4">
131+
<p class="text-eyebrow mt-0">Release notes</p>
132+
<p class="text-title">Faster builds, calmer pages</p>
133+
<p class="text-lead">This release cuts rebuild times in half and refines how pages load.</p>
134+
<p class="text-attribution mb-0">PureStack team</p>
135+
</div>
136+
137+
```mdx
138+
<p class="text-eyebrow">Release notes</p>
139+
<p class="text-title">Faster builds, calmer pages</p>
140+
<p class="text-lead">This release cuts rebuild times in half and refines how pages load.</p>
141+
<p class="text-attribution">PureStack team</p>
142+
```
143+
144+
| Class | Based on | Treatment |
145+
| --- | --- | --- |
146+
| `text-eyebrow` | `xs`, `w700` | Uppercase, wide letter spacing, subtle color |
147+
| `text-title` | `h2`, `w700` | Tight line height |
148+
| `text-lead` | `h4` | Relaxed line height for introductions |
149+
| `text-tagline` | `sm` | Subtle color |
150+
| `text-quote` | `h5`, `w600` | Italic |
151+
| `prose-pullquote` | `h3`, `w100` | Light italic |
152+
| `text-attribution`, `prose-meta` | `xxs`, `w600` | Uppercase, subtle color |
153+
| `text-caption` | `h3` | Subtle color |
154+
| `text-subtle` | Inherited | Subtle color |
155+
156+
Roles style an element; they do not change what it is. A `text-title` paragraph is still a paragraph, so use a real heading when the text starts a section. Roles also set their own bottom margin. Use `m-0` or `mb-0` when they sit inside a tight layout.
157+
158+
### Larger reading contexts
159+
160+
Add `large-fs` to a container to scale up the `fs-*`, `fw-*`, and text role classes inside it, along with spacing utilities, for example in a presentation slide or a kiosk view. `medium-fs` restores the normal scale inside such a container. Both classes act on descendants and do not respond to screen width.
47161

48-
## Customize the scale
162+
## Font families
49163

50-
Set shared typography values in a skin, then use the palette in typed styles:
164+
The skin's `font.family.base` sets the font for the doc template and everything inside it. Code always uses a separate monospace stack that is not part of the skin.
165+
166+
The family is an ordinary CSS font stack:
167+
168+
| Skin | `font.family.base` |
169+
| --- | --- |
170+
| Standard | `'Manrope', 'Segoe UI', system-ui, sans-serif` |
171+
| Studio | `'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif` |
172+
173+
**PureStack does not download fonts.** The browser uses the first family in the stack that it can find, either installed on the visitor's device or loaded by your page. To use a web font, load it yourself, for example from the `head` of a custom page template:
174+
175+
```ts
176+
head.push(
177+
h('link').attr({ rel: 'stylesheet', href: '/assets/fonts/inter.css' }),
178+
)
179+
```
180+
181+
Always end the stack with fallbacks such as `system-ui` and `sans-serif`, so text stays readable before the font arrives or when it is unavailable.
182+
183+
## Customize the type
184+
185+
### In the site configuration
186+
187+
To change a few values, override the font settings in `siteConfig.json`. Each theme mode has its own palette, so set the values for both `light` and `dark`:
188+
189+
```json
190+
{
191+
"style": {
192+
"theme": {
193+
"skin": "standard",
194+
"palette": {
195+
"light": {
196+
"font": {
197+
"family": { "base": "'Source Sans 3', system-ui, sans-serif" },
198+
"size": { "body": "1.0625rem", "h4": "1.125rem" }
199+
}
200+
},
201+
"dark": {
202+
"font": {
203+
"family": { "base": "'Source Sans 3', system-ui, sans-serif" },
204+
"size": { "body": "1.0625rem", "h4": "1.125rem" }
205+
}
206+
}
207+
}
208+
}
209+
}
210+
}
211+
```
212+
213+
Values you leave out keep the skin's settings.
214+
215+
### In a skin
216+
217+
When you build your own skin, define the type once and merge it into both modes:
51218

52219
```ts
53-
import { themeSkins, type ThemePalette } from '@purestack/ts-style'
54-
import { merge } from '@purestack/ts-util'
220+
import { registerSkin, type ThemePalette, themeSkins } from '@purestack/ts-style'
221+
import { type DeepPartial, merge } from '@purestack/ts-util'
55222

56-
const light: ThemePalette = merge(themeSkins.standard.create().light, {
223+
const type = {
57224
font: {
58-
family: { base: "'Inter', system-ui, sans-serif" },
59-
size: { body: '1rem', h2: '2rem' },
225+
family: { base: "'Source Sans 3', system-ui, sans-serif" },
226+
size: {
227+
body: '1.0625rem',
228+
h4: '1.25rem',
229+
h3: '1.5rem',
230+
h2: '2rem',
231+
h1: '2.75rem',
232+
display: '4rem',
233+
},
234+
weight: { w700: '650' },
235+
},
236+
} satisfies DeepPartial<ThemePalette>
237+
238+
registerSkin('my-site', {
239+
create: () => {
240+
const standard = themeSkins.standard.create()
241+
return {
242+
light: merge(standard.light, type),
243+
dark: merge(standard.dark, type),
244+
}
60245
},
61246
})
62247
```
63248

64-
The same `font` overrides can be shared with the dark palette. Register the resulting pair as a skin, then select it in `siteConfig.json`; the [Themes guide](/guides/themes/) shows the full flow.
249+
Keep the scale in order from `xxxs` up to `display`, with each step at least as large as the one before it. The [Themes guide](/guides/themes/) shows how to select a registered skin.
250+
251+
### Change the root size
252+
253+
Because every size is in `rem`, the root font size scales the whole scale at once. `remSize` sets it for all screens, and `mobileRemSize` sets it below the medium breakpoint:
254+
255+
```json
256+
{
257+
"style": {
258+
"theme": {
259+
"skin": "standard",
260+
"mobileRemSize": "15px"
261+
}
262+
}
263+
}
264+
```
265+
266+
Leave both empty to respect the visitor's browser setting. When you do set them, prefer small adjustments. Visitors who enlarge text in their browser still expect the page to follow.
267+
268+
## Use the scale in your own styles
269+
270+
Custom styles can read the same scale. Pass a size and weight to `applyFont` to get the derived line height and letter spacing too:
271+
272+
```ts
273+
import { styleBuilder, themes } from '@purestack/ts-style'
274+
275+
themes.forEach((theme, palette) => {
276+
styleBuilder
277+
.get(theme)
278+
.select('.metric-value')
279+
.apply(palette.applyFont(palette.font.size.h1, palette.font.weight.w700))
280+
281+
styleBuilder
282+
.get(theme)
283+
.select('.metric-label')
284+
.apply(palette.applyFont(palette.font.size.xs, palette.font.weight.w600))
285+
.color(palette.current.text.subtle)
286+
})
287+
```
65288

66-
For controls, prefer the component's own size and variant props. The [component catalog](/components/) shows how each control responds to the active theme.
289+
Use names from the scale instead of fixed values like `'28px'`. Your styles then follow the skin, and a size change in the skin updates them along with everything else.

0 commit comments

Comments
 (0)