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.

The page frame

A <main> with no class of its own gets a measure, a gutter and top and bottom padding, because a bare document rendered edge to edge at 130 characters a line is unreadable:

<main>
  <h1>A page</h1>
  <p>This is already laid out.</p>
</main>

Put a class on the element and the frame steps out of the way, on the assumption that you have taken over the layout. .page-shell and the app-shell classes in layout supply their own.

A page with no <main> in it at all gets the same frame on its <body> instead, so that a page written as bare markup still reads as a document. That arm is narrow on purpose: it applies only when the body has no class of its own and the page contains no <main>. Give the body a class, or put a <main> anywhere in the page, and it does not apply. elements create page writes a <main> for you, so a generated page is already on the first path.

A page is a template named <html>, so <html class="chat"> puts chat on the <html> element, not on the body:

<html class="chat">
  <main>
    <h1>A page</h1>
  </main>
</html>

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.

Inside that frame, running text stops at --measure (68ch): paragraphs, list items, quotes, definitions and captions all cap there, so a wide document still reads well even though the frame itself is wide enough for a table.

The cap is scoped to the frame on purpose. Capping every paragraph on every page would reach into your components, where it is the wrong call often enough to matter and silent when it is: a paragraph in a text-align: center hero keeps its capped box flush left and renders off-centre from the heading above it. Put a class on the main and your paragraphs are your own.

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

h1h6 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 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.