Typography
The single reference for type in Wildflower — families, weights, the size scale, heading styles, and spacing. If you are picking a font size, this page is the place to look it up.
Source of truth: packages/web-tailwind/partials/typography.css.
Reach for a utility, not a raw size. heading-*, text-body, and
text-mono bundle family, size, weight, and line height into one class and
stay responsive. Use raw text-* sizes only when no utility fits.
Font families
Three families, all loaded from a single Typekit kit in src/app/layout.tsx. Each font-* utility maps to a --font-* custom property declared in @theme.
| Utility | Family | Weights available | Use |
|---|---|---|---|
font-display | polymath | 400, 600 | Headings and display type |
font-sans | polymath-text | 400, 500, 700 (+ italics) | Body copy, UI labels — the default |
font-mono | ibm-plex-mono | 400, 800 | Code, token names, eyebrows |
Every family falls back through -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'segoe ui', 'Ubuntu', 'roboto', 'noto', Arial (or ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas for mono).
Headings and display type. Paired with every heading-* utility.
Body copy, UI labels, and anything not a heading. The default.
Code, token names, and the tag utility.
Only the weights above actually exist. Asking for a weight the kit does
not ship gets you a browser-synthesized approximation, which looks subtly
wrong. Two mismatches to know about: font-display has no 500 or 700, so
font-medium and font-bold on a heading are not real weights — use
font-semibold (600) or font-normal (400). And font-mono has no 500, 600,
or 700, so the common eyebrow recipe font-mono ... font-semibold renders at
400. Use font-normal or font-extrabold (800) with mono.
dbt Summit faces
Three extra display faces exist for the dbt Summit microsite and Summit campaign blocks. Do not use them in general marketing pages.
| Utility | Family |
|---|---|
font-summit-area | area-normal |
font-summit-inter | inter-variable |
font-summit-bugrino | bugrino |
bugrino is self-hosted via an @font-face rule in typography.css (weight 500) so it works outside the Summit layout, which loads it through next/font.
Type scale
Twelve steps, declared as --text-* properties in @theme. Sizes are in rem and assume a 16px root.
| Utility | rem | px | Default line height |
|---|---|---|---|
text-xs | 0.75rem | 12px | 1.333 → 16px |
text-sm | 0.875rem | 14px | 1.429 → 20px |
text-base | 1rem | 16px | 1.5 → 24px |
text-lg | 1.125rem | 18px | 1.556 → 28px |
text-xl | 1.25rem | 20px | 1.4 → 28px |
text-2xl | 1.5rem | 24px | 1.333 → 32px |
text-3xl | 2rem | 32px | 1.2 → 38.4px |
text-4xl | 2.25rem | 36px | 1.111 → 40px |
text-5xl | 3rem | 48px | 1 → 48px |
text-6xl | 4rem | 64px | 1 → 64px |
text-7xl | 4.5rem | 72px | 1 → 72px |
text-8xl | 5.5rem | 88px | 1 → 88px |
Three steps are deliberately retuned from Tailwind's stock scale — text-3xl (30px → 32px), text-6xl (60px → 64px), and text-8xl (96px → 88px). If a design comp says 32px, that is text-3xl here, not text-4xl.
Raw text-* classes set a line height you did not choose. Tailwind pairs
each size with a default ratio, and those ratios are inherited from Tailwind's
original scale rather than tuned for ours. Anything typographically important
should set leading-* explicitly or use a heading-* / text-body utility.
Headings
Six heading utilities. Each one applies font-display, a size, font-semibold, and a line height — and all but heading-5 step up a size at the xl breakpoint (1280px).
| Utility | Below xl | At xl and up | Line height | Use |
|---|---|---|---|---|
heading-display | text-6xl / 64px | text-8xl / 88px | 1 | Hero statements |
heading-1 | text-5xl / 48px | text-6xl / 64px | 1.2 | Page title |
heading-2 | text-3xl / 32px | text-5xl / 48px | 1.2 | Major sections |
heading-3 | text-2xl / 24px | text-3xl / 32px | 1.2 | Subsections, card titles |
heading-4 | text-xl / 20px | text-2xl / 24px | 1.2 | Nested subsections |
heading-5 | text-lg / 18px | — | 1.2 | Eyebrows, labels |
The live specimens below render at your current viewport width, so widen or narrow the window to see the xl step.
How data gets done
Hero statements. One per page, at most.
How data gets done
Page title. One per page.
How data gets done
Major section headings.
How data gets done
Subsections and card titles.
How data gets done
Nested subsections, smaller card titles.
How data gets done
Eyebrows, labels, and the smallest heading rung.
Heading utilities are not heading tags
Appearance and document structure are separate decisions. Pick the tag from the page outline and the appearance from the design.
Use the base components in packages/web-ui/components/Elements.tsx. Nine times out of ten these are all you need — each one pairs a heading-* utility with its matching semantic tag, and the as prop overrides the tag when the outline calls for something different.
| Component | Default tag | Applies |
|---|---|---|
SuperH1 | <h1> | heading-display |
H1 | <h1> | heading-1 |
H2 | <h2> | heading-2 |
H3 | <h3> | heading-3 |
H4 | <h4> | heading-4 |
H5 | <h5> | heading-5 |
H6 | <h6> | text-xs md:text-sm font-bold (not a heading-* utility) |
import { H2, SuperH1 } from '@dbt-labs/web-ui'
{/* Standard case — appearance and tag already agree */}
<H2>How data gets done</H2>
{/* A page title that should look smaller than heading-1:
keep the h1 tag, borrow the heading-2 appearance */}
<H2 as='h1'>Pricing</H2>
{/* A visually large statement that is not the page title */}
<SuperH1 as='h2'>How data gets done</SuperH1>The same file also exports Tag (a span with the tag utility), UL, and OL.
Props: as sets the rendered tag, className is appended to the component's default classes, and customID overrides the id. Every component derives an id from its own text content by default, so headings are deep-linkable for free.
Because className is appended rather than merged, a utility that collides
with one of the component's defaults will not reliably win on specificity
alone — this is why you will see ! flags in existing code, e.g.
<H5 as='h2' className='!mb-2'>. Use ! when you need to override a default.
Reach for a raw heading-* class only in the rare case where you are adjusting text outside these components and the as and className props cannot express it. Prefer the components by default.
Keep one h1 per page and do not skip levels in the tag hierarchy.
Body and mono text
| Utility | Applies |
|---|---|
text-body | font-sans + text-base (16px) + leading-[1.5] |
text-mono | font-mono + text-base (16px) + leading-[1.5] |
text-body is the default for paragraph copy. For secondary or dense UI text, drop to text-sm with an explicit leading-[1.5].
text-body and text-mono are currently declared twice — once in
partials/typography.css and again in partials/tokens.css. Both definitions
are identical, so there is no behavioral difference today, but edit both or
neither until the duplication is removed.
Pair text color with a semantic token
Size and color are separate decisions. Use the semantic text tokens rather than raw palette values.
<p className='text-body text-neutral-secondary'>
Body copy in the standard secondary text color.
</p>See Design tokens for the full list.
Line height
There is no leading-* theme override, so Tailwind's stock leading utilities apply. The type system uses only two ratios in practice:
| Context | Line height |
|---|---|
Headings (heading-1 through heading-5) | leading-[1.2] |
heading-display | leading-[1] |
Body and mono (text-body, text-mono) | leading-[1.5] |
| Uppercase eyebrows | leading-[1.1] |
Rules of thumb: as type gets larger, leading gets tighter. Body copy needs 1.5 for readability at long measures; display type at 64px and up needs 1 or the lines drift apart.
Letter spacing
Wildflower does not override Tailwind's tracking-* scale, and normal text — body copy and headings alike — should use default tracking. Never letter-space lowercase body copy.
The one established exception is uppercase eyebrow labels, where tracking is required to keep all-caps legible. The de-facto recipe across the site is:
<p className='font-mono text-xs font-normal uppercase tracking-[0.2em] text-brand-primary'>
Case study
</p>Tracking values in use for eyebrows range from tracking-[0.1em] to tracking-[0.2em] (tracking-widest is 0.1em). Prefer tracking-[0.2em] for small mono eyebrows and tracking-[0.1em] at text-sm and above.
This eyebrow pattern is a convention, not a utility — there is no eyebrow
class in the theme. Copy the recipe above rather than inventing new tracking
values, and note the font-normal: mono has no semibold.
Text spacing and measure
The theme provides no vertical-rhythm utility, so spacing between text elements is set per layout with standard Tailwind spacing. Use these defaults for consistency.
Vertical spacing
| Relationship | Spacing |
|---|---|
| Eyebrow → heading | mb-3 (12px) |
| Heading → body copy | mb-4 (16px) |
| Between paragraphs | space-y-4 (16px) |
| Between grouped text blocks | space-y-6 (24px) |
| Between subsections | space-y-8 (32px) |
| Between page sections | handled by Section — py-12 md:py-16 |
Prefer a space-y-* on the container over margins on individual children — it keeps the first and last child flush with the container edges.
<div className='space-y-6'>
<div>
<p className='mb-3 font-mono text-xs uppercase tracking-[0.2em] text-brand-primary'>
Platform
</p>
<h2 className='heading-2 mb-4'>How data gets done</h2>
<p className='text-body text-neutral-secondary max-w-2xl'>
Body copy, constrained to a readable measure.
</p>
</div>
</div>Measure (line length)
Long lines are hard to read. Constrain body copy to a maximum width rather than letting it span a full 12-column grid.
| Constraint | Width | Use |
|---|---|---|
max-w-2xl | 42rem / 672px | Standing default for body copy |
max-w-3xl | 48rem / 768px | Wider intro or lede paragraphs |
max-w-4xl | 56rem / 896px | Headings that need to break across lines |
Aim for 60–80 characters per line. Headings can run wider than body copy because they are short.
Choosing a size
- Is it a heading? Use a base component from
Elements.tsx(H1–H6,SuperH1), chosen by hierarchy, not by pixel value — withasif the tag needs to differ. - Is it paragraph copy? Use
text-body, constrained withmax-w-2xl. - Is it code, a token, or an eyebrow? Use
font-monowith an explicit size. - Is it dense or secondary UI text? Use
text-smwithleading-[1.5]and atext-neutral-tertiarytoken. - Only if none of the above fit, use a raw
text-*size — and setleading-*explicitly.
Source files
| File | Purpose |
|---|---|
packages/web-tailwind/partials/typography.css | Font families, --text-* scale, heading-*, text-body, text-mono, tag |
packages/web-tailwind/partials/tokens.css | Semantic text color tokens (plus duplicate text-body / text-mono) |
packages/web-tailwind/partials/components.css | link-styles, focus-styles, form input type styles |
packages/web-tailwind/theme.css | CSS entry point that imports all partials |
packages/web-ui/components/Elements.tsx | H1–H6, SuperH1, Tag, UL, OL — the preferred way to render headings |
apps/wildflower/src/app/layout.tsx | Typekit stylesheet that loads the families |