Typography
Foundations
All font sizes output in rem, converted from px against $base-font-size (16px). No root font-size is written to html for the default 16px โ an author font-size there would override the reader's browser default-size setting and pin every rem in the stylesheet, which is the one thing rem output exists to avoid. Set $base-font-size to something else and the baseline moves as a percentage, which still tracks that setting. See Configuration.
Letter-spacing is the exception: px values convert to em, against that rule's own font-size rather than the root. Tracking corrects a size, so it has to move when the size does โ .supertitle is 2px at 14px, and it stays that ratio under .fs-32 instead of staying 2px.
Default fonts:
| Variable | Default |
|---|---|
$heading-font |
'Roboto', sans-serif |
$paragraph-font |
'Nunito', sans-serif |
$mono-font |
monospace |
body uses $paragraph-font. h1โh6 elements override to $heading-font.
Default line-heights:
| Variable | Value |
|---|---|
$line-height |
1.6 (html root) โ $line-heights.normal |
$heading-line-height |
1.25 โ $line-heights.tight |
$paragraph-line-height |
1.6 โ $line-heights.normal |
All three read out of the $line-heights map rather than carrying their own number, so retuning the map moves the type scale and the .lh-* utilities together. Override a variable directly to break one out of the map.
Type scale
Headings
h1โh3 have two size sets: mobile (default, no media query) and desktop (min-width: 1024px, the lg breakpoint). h4โh6 have one โ at 20px and below there is nothing to give back on a small screen. The semantic tag and its class equivalent share identical rules โ .h1 gives heading styling without the <h1> element.
Each heading entry: (font-size, letter-spacing, line-height, font-weight). Sizes output as rem, letter-spacing as em.
| Selector | Desktop size | Mobile size | line-height | font-weight |
|---|---|---|---|---|
h1, .h1 |
3rem (48px) | 2rem (32px) | 1.25 | bold |
h2, .h2 |
2rem (32px) | 1.5rem (24px) | 1.25 | bold |
h3, .h3 |
1.5rem (24px) | 1.25rem (20px) | 1.25 | bold |
h4, .h4 |
1.25rem (20px) | โ | 1.25 | bold |
h5, .h5 |
1rem (16px) | โ | 1.25 | bold |
h6, .h6 |
0.875rem (14px) | โ | 1.25 | bold |
This is a reading ladder, not a poster one: it opens a document rather than a hero. h5 sits at body size and h6 below it, carrying their level through weight and family instead of scale โ the same collapse GitHub's and most docs renderers' prose use. For display type, reach for .fs-64 / .fs-96, which are still in $font-sizes.
No letter-spacing is set on any heading or paragraph. Tracking corrects a specific face at a specific size; applied blind across a scale it fights whatever $paragraph-font is actually set to. .supertitle is the exception, since capitals are drawn to be set apart.
Heading elements also carry default bottom margins (applied to the tag, not the class):
| Element | margin-bottom |
|---|---|
h1 |
0.33em |
h2 |
0.42em |
h3 |
0.50em |
h4 |
0.65em |
h5 |
0.80em |
h6 |
1.00em |
Example โ same visual output, different semantics:
<h2>Section title</h2>
<div class="h2">Section title</div>Paragraph and body text
Non-heading entries use a single size set (no responsive switch). letter-spacing values in px are converted to em against the same entry's font-size.
| Selector | font-size | letter-spacing | line-height |
|---|---|---|---|
.p1 |
1.5rem (24px) | โ | 1.6 |
.p2 |
1.25rem (20px) | โ | 1.6 |
p |
1rem (16px) | โ | 1.6 |
.p3, figcaption |
0.875rem (14px) | โ | 1.6 |
.p4, small |
0.75rem (12px) | โ | 1.6 |
.supertitle |
0.875rem (14px) | 0.1428571429em (2px at its own 14px) | 1.6 |
.supertitle additionally sets font-weight: 500 and text-transform: uppercase.
<p class="p1">Large body copy</p>
<p class="p2">Medium body copy</p>
<p>Default paragraph โ 1rem / 16px</p>
<figcaption>Caption text (same as .p3)</figcaption>
<small>Fine print (same as .p4)</small>
<p class="supertitle">Label / eyebrow text</p>Font size utilities โ .fs-*
Generated from $font-sizes. px in the class name, rem in the output โ same
convention as .pt-32, same toRem() conversion the type scale uses, so
.fs-24 and .p1 land on exactly the same size.
| Class | font-size |
Matches |
|---|---|---|
.fs-12 |
0.75rem (12px) | .p4, small |
.fs-14 |
0.875rem (14px) | .p3, figcaption |
.fs-16 |
1rem (16px) | p |
.fs-20 |
1.25rem (20px) | .p2, h4 |
.fs-24 |
1.5rem (24px) | .p1, h3 desktop |
.fs-32 |
2rem (32px) | h2 desktop |
.fs-48 |
3rem (48px) | h1 desktop |
.fs-64 |
4rem (64px) | display only |
.fs-96 |
6rem (96px) | display only |
This is the nudge that does not need a new component class โ the type scale
still owns letter-spacing, line-height and weight, .fs-* changes size only.
Responsive variants included:
<h2 class="fs-32 fs-md-64">Smaller on phones</h2>
Line height utilities โ .lh-*
Generated from $line-heights. Unitless values on purpose: a unitless
line-height inherits as a ratio, so children scale with their own font-size;
a px one inherits as a frozen box and collapses nested type.
| Class | line-height |
|---|---|
.lh-1 |
1 |
.lh-tight |
1.25 โ headings |
.lh-normal |
1.6 โ body copy, html root |
.lh-loose |
1.75 |
tight and normal are not only utility values: $heading-line-height and $paragraph-line-height read them out of this map, so a change here moves the type scale with the classes.
Pairs with .fs-* โ bumping size usually means loosening or tightening leading:
<p class="fs-32 lh-tight">Display text needs less leading than body copy.</p>Font family utilities
Override the font family on any element. All use !important.
| Class | CSS |
|---|---|
.font-heading |
font-family: 'Roboto', sans-serif |
.font-paragraph |
font-family: 'Nunito', sans-serif |
.font-mono |
font-family: monospace |
Font weight utilities
Weight lives under font-, matching the CSS property. All use !important.
| Class | font-weight |
|---|---|
.font-thin |
100 |
.font-ultra-light |
200 |
.font-light |
300 |
.font-normal |
400 |
.font-medium |
500 |
.font-semi-bold |
600 |
.font-bold |
700 |
.font-ultra-bold |
800 |
.font-black |
900 |
Warning
The .text-* weight aliases (.text-bold, .text-heavy, โฆ) were removed in 3.0.0. text-* is reserved for real text- properties โ text-align, text-transform, text-decoration. Rename to the font-* form; .text-heavy becomes .font-black.
The relative lighter / bolder classes are gone too. They resolved against the parent's weight, so the same class produced a different weight depending on where it landed โ the one thing a utility class must not do. Pick the numeric class you actually want.
Text alignment
Generated with responsive variants (all breakpoints). Base classes apply at all widths; breakpoint-infixed variants apply at min-width.
.text-left โ text-align: left
.text-center โ text-align: center
.text-right โ text-align: right
Responsive examples:
.text-md-center โ text-align: center (min-width: 768px)
.text-lg-left โ text-align: left (min-width: 1024px)
Text decoration
.text-underline โ text-decoration: underline
.text-overline โ text-decoration: overline
.text-line-through โ text-decoration: line-through
.text-decoration-none โ text-decoration: none
Responsive variants follow the same breakpoint-infix pattern as alignment.
Text transform
.text-uppercase โ text-transform: uppercase
.text-lowercase โ text-transform: lowercase
.text-capitalize โ text-transform: capitalize
.text-full-width โ text-transform: full-width
.text-full-size-kana โ text-transform: full-size-kana
.text-transform-none โ text-transform: none
Responsive variants available for all breakpoints.
Note
full-width and full-size-kana are CJK typographic values โ full-width
converts halfwidth latin and kana to their fullwidth forms, full-size-kana
converts small kana to full-size (a ruby-text convention). Both are no-ops on
latin-only text.
Font style
.font-italic โ font-style: italic
.text-italic was removed in 3.0.0 โ font-style is a font- property, so it belongs in the font-* namespace with the weights.
Whitespace
.text-nowrap โ white-space: nowrap
Text wrapping
.text-balance โ text-wrap: balance
.text-pretty โ text-wrap: pretty
balance evens out line lengths across a block โ the fix for a heading whose
last line is one orphaned word. Browsers cap it at a handful of lines
(4โ6), so it is a headline tool, not a body-copy one. pretty is the body-copy
counterpart: it only prevents orphans, with no line limit.
Not responsive, and there is no text-wrap: nowrap class โ .text-nowrap above
is the white-space one, and two classes a character apart is a trap.
Truncation
.truncate โ overflow: hidden; white-space: nowrap; text-overflow: ellipsis
Single-line ellipsis. The element needs a width to overflow against โ a block
element inherits one from its parent, but a flex or grid child does not shrink
below its content by default, so pair it with .min-w-0 there:
<div class="d-flex">
<span class="truncate min-w-0">A very long label that gets cut offโฆ</span>
</div>For more than one line, use .line-clamp-{n} โ generated from $line-clamps
(1, 2, 3, 4, 5, 6), not responsive.
.line-clamp-3 โ display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: 3; line-clamp: 3; overflow: hidden
The clamp is implemented with display: -webkit-box, which is still the only
form every supported browser understands โ the unprefixed line-clamp is
emitted alongside it for the ones that do. Two consequences:
displayis part of the mechanism, so putting.d-blockor.d-flexon the same element switches the clamp off. Clamp the inner element instead.- There is no
.line-clamp-none. Removing the class is the way to unclamp; if you need to undo it at a breakpoint, swap in a.d-*class there.
List utilities
From _list.scss. Strips default list styling.
ul.reset,
ol.reset {
list-style: none;
margin: 0;
padding: 0;
text-indent: 0;
}
The .reset class must be applied directly on the <ul> or <ol> element (qualified selector).
<ul class="reset">
<li>No bullets, no indent</li>
</ul>