Base elements
elements man style/base Read as markdownBare HTML elements style themselves. Write semantic markup (<h1>, <p>,
<a>, <ul>, <table>, <code>) and it looks right with no classes. This is
base.css in @layer base.
Because base rules sit in the base layer (below components and utilities),
any class you add wins automatically. There's no specificity battle: the layer
order does the work, so you never need !important to override a base style.
Spacing
Flow content is spaced for you. Headings, paragraphs, lists, quotes, tables, figures, forms and rules carry vertical margins, so an ordinary page reads as a document without a single class.
Margins collapse between siblings, so a heading's bottom margin and the
following paragraph's top margin resolve to the larger of the two rather than
adding up. First and last children reset their outer margin, so a padded
container (a .card, a .callout) never gains a phantom band of space inside
its own padding.
The layout primitives space their children with gap instead, so they clear
those margins: .stack, .cluster and .row all zero the flow margins of
their direct children. A gap-based layout never doubles up, and you don't have
to know that to get it right.
Width is a class, never a default
This file styles elements. It sets no width, no centering and no page padding on anything, so nothing here can move your layout and nothing changes when you add a class.
Width comes from layout, where it is named and visible in the markup:
<main class="page-shell">
<h1>A page</h1>
<p>Measured, centred, with a gutter.</p>
</main>
.page-shell is 72rem. .is-prose narrows it to 65ch for an article,
.is-narrow, .is-marketing and .is-wide are the other stops, and
.is-full removes the cap. elements create page writes .page-shell for
you, so a generated page is already laid out.
A <main> with no class gets no width. That is the point: a class adds
behavior and never removes it, so you never have to know not to write one.
Until 0.3.0 the opposite was true. A <main> with no class picked up a frame,
and a classless <body> on a page with no <main> picked one up too. Both
fired on the absence of a class, which made three things go wrong at once:
putting any class on a <main> silently deleted the page's layout; a page is a
template named <html>, so the page class landed there and left <body> bare
for the second rule to catch; and the companion --measure rule reached down
into components. If your page loses its gutter on 0.3.0, add .page-shell.
A page is a template named <html>, so <html class="page-chat"> puts
page-chat on the <html> element, not on the body:
<html class="page-chat">
<main>
<h1>A page</h1>
</main>
</html>
elements create page chat writes the page- prefix. A bare .chat there
would share a namespace with every component class in the app, and a .chat
rule written for a widget would match <html> and restyle the whole document.
Treat that class as a scope for custom properties and theming, not as a layout
hook. Layout written against it applies to <html>, which makes <body> a
child box of whatever you declared: give <html> a grid and a height and the
body becomes a shrink-to-fit grid item. Put layout on an element inside the
page instead, and reach for .app in layout when you want a
full-height application shell.
Nothing caps the width of running text. A measure is a property of the box
that wants one, so .page-shell.is-prose is 65ch wide and the text inside it
is measured by the container.
There used to be a rule that capped every paragraph, list item, quote,
definition and caption inside an unclassed <main>. It reached into
components, where it was silently wrong often enough to matter: a paragraph in
a text-align: center hero kept its capped box flush left and rendered
off-centre from the heading above it.
Colored containers
Put anything in a container that sets a color and the text inside inherits
it, headings and paragraphs included:
<div style="background: #0b1f30; color: white">
<h2>Readable</h2>
<p>So is this.</p>
</div>
This works because base rules set color on almost nothing. html and body
supply --ink for the document, and everything below them inherits. Only
elements that carry their own background set a color of their own (code,
pre, kbd, mark, ::selection). Those own both sides of the pairing, so
they stay legible instead of floating with the surroundings.
Elements that are meant to read softer than body text (dd, blockquote,
th, figcaption) use --ink-soft-flow, which is derived from
currentColor. They stay soft on a normal page and still follow the container
on a colored one.
Links adapt too, taking their lightness from the text around them. See links below.
Document
html and body set the sans font, base font size, body line height, --ink
text, and --bg background. Dark mode flips both through the tokens.
Headings
h1–h6 form a modern scale, with semibold, tight leading, tightened
tracking on the larger sizes:
| Element | Size |
|---|---|
h1 |
--text-4xl (48px) |
h2 |
--text-3xl (36px) |
h3 |
--text-2xl (30px) |
h4 |
--text-xl (24px) |
h5 |
--text-lg (20px) |
h6 |
--text-base (16px) |
Links
Bare <a> renders as an underlined link with a soft underline that solidifies
on hover and a focus ring on :focus-visible. The rule is wrapped in
:where() so link component classes (.button, .link-action, and the like)
override it without a specificity fight, even within the same layer.
The color adapts to the surface. A link takes its lightness from the text
around it, nudged a fifth further from mid-grey so it reads as a link rather
than as a tinted word, and its hue and chroma from --link-c and --link-h.
The result is readable on a white page, on a dark panel and on a saturated
brand hero with no class and no per-surface tokens:
<div style="background: #0b1f30; color: white">
<p>A <a href="/">link</a> here is light, because the text around it is.</p>
</div>
A fixed color cannot do that. Measured against a dark panel, a pinned accent lands at 1.21:1 in the default theme, which is invisible.
The chroma is capped to what sRGB can show at the link's lightness, so a link never asks for a color outside the gamut and every browser paints the same one. A light link on a dark surface is paler than the brand for that reason, and a link inside white text is white with an underline.
The default theme leaves --link-c and --link-h unset, so links render as
the surrounding ink a shade deeper, plus the underline. That is the monochrome
default and it is always legible. Set both to brand your links; see
branding.
Focus
Anything focusable shows a ring. Links, buttons, inputs, summary and the
interactive components each draw their own; everything else, including an
element you make focusable with tabindex, falls back to a 2px --focus
outline from the base layer. You cannot ship a focusable control with no
visible focus state by forgetting to style one.
Forms
A bare form is a stack of labels and controls with no wrapper, so base makes
the form itself a one-column grid and spaces it with a gap: fields are
separated, and a label sits tight against the control it names.
The spacing is a gap and not a margin so that it goes away the moment you lay
the form out yourself. Give a form display: flex to put an input beside its
button and you get exactly that, with no leftover vertical step under the
button and no class to remember. form.is-grid is the same idea in two
columns.
<form>
<label for="email">Email</label>
<input id="email" type="email">
<button type="submit">Sign up</button>
</form>
For validation states, hints, inline errors and grouped controls, use the form components in form.
Lists
A bare ul gets discs, a bare ol gets decimals, both indented, with nested
lists stepping through circle and square. The reset strips markers first so a
nav or a menu built from <ul> starts clean, and the layout primitives strip
them again for exactly that case: <ul class="cluster"> is a row of links, not
a bulleted list.
Definition lists render dt semibold and dd indented in --ink-soft-flow.
Quotes and rules
blockquote gets a left rule and italic --ink-soft-flow text. hr is a single
--rule top border.
Code, pre, kbd
code: monospace, on a--bg-mutedfill with small padding and--radius-xs.pre: monospace block on--bg-inversewith--ink-inversetext,--radius-surface, and horizontal scroll.pre coderesets its own fill so nested code doesn't double up.kbd: a small keycap: monospace, muted fill, a--ruleborder and a subtle bottom shadow.
<pre> uses --bg-inverse, so it flips with the theme (dark block in light
mode, light block in dark mode). The terminal component is different: it stays
dark in both modes. See terminal.
Icons
Size every svg on the tag, with width and height. An svg carrying only
a viewBox fills whatever contains it, so an icon paints full-page for as long
as it takes its CSS to arrive. Attributes hold before any stylesheet does,
which is what makes them the right place for a size.
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
Mark a decorative icon aria-hidden="true". One that carries meaning on its
own needs a label instead.
Mark
mark is a highlight using the warning tint (--warning-soft on
--warning-ink).
Tables
table is full-width with small text and collapsed borders. thead gets a
bottom rule, body rows get soft top rules between them, th is left-aligned
semibold --ink-soft-flow, and td is top-aligned with comfortable padding.
figcaption renders as small --ink-soft-flow caption text.
For a more editorial table or long-form content, wrap it in .prose (see
typography).
Selection
::selection tints the highlight with the accent color, so text selection picks
up your brand once you've branded the app.