Manual Style Base elements

Base elements

elements man style/base Read as markdown

Bare 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-muted fill with small padding and --radius-xs.
  • pre: monospace block on --bg-inverse with --ink-inverse text, --radius-surface, and horizontal scroll. pre code resets its own fill so nested code doesn't double up.
  • kbd: a small keycap: monospace, muted fill, a --rule border 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.