Tokens
The first of LoamUI's three primitives: a handful of semantic custom properties that every component reads. A small palette of colours and neutrals, plus fluid scales: everything else is derived by formula, so theming is overriding --loam-* values at any scope. No ThemeProvider, no JavaScript.
The surface
Small enough to read in full. A component library that mints thousands of tokens makes every one a decision nobody can audit; here the decisions are the hues and neutrals, and the rest is arithmetic:
Brand & status
The colour decisions a theme makes. Primary is neutral (black/white) by default, the white-label starting point; accent is the chromatic flourish; success/danger/warning/info are the status hues; link and highlight round out the platform defaults.
Neutrals
Surfaces, text and borders.
Derived
Each hue derives its soft tint and solid fill through the same formulas. The focus ring uses the solid fill. Rebrand a hue and its family follows.
Fonts
Two families a theme can swap (body and display), plus a monospace for code. System by default.
Type scale
Fluid clamp() values in container units: they respond to the nearest container, or the viewport without one.
Spacing
The same fluid construction, growing against the container rather than the viewport; minimums are the fixed values, so nothing shrinks below them.
Fixed spacing
The deliberate opposite: for a hairline, a ring inset, the gap beside an icon. Reach for the fluid scale first.
Radius & motion
Deliberately fixed: rounding and durations don't breathe with the viewport.
Elevation
Layered light-dark() shadows: a real drop in light; in dark the drop goes transparent and an inset top highlight carries the bevel, so the light source stays consistent.
Values are read live from the loaded stylesheet with getComputedStyle, in your current colour scheme; fluid values show their computed size at this viewport.
Rebrand in one line
One token carries the brand: --loam-color-primary. Everything else (the soft tint, hover, active,
solid fills, focus rings) is derived from it by formula, so there is nothing else to keep in
sync:
@layer loamui.tokens { :root { --loam-color-primary: light-dark(oklch(0.62 0.2 275), oklch(0.72 0.17 275)); /* violet */ }}
Load this theme stylesheet after the core stylesheet so its token declaration follows
the default in the same layer. App-wide tokens belong on :root; a composition's
layout and treatment belong in scoped loamui.components rules.
The brand colour appears wherever the design says primary: focus rings, checked states, carets,
and primary context regions. (Buttons are neutral by default; the demo below is
wrapped in a primary region so you can see the change.) Scope the token to a subtree to theme
just part of a page: the nearest declaration up the tree wins, so both cards below run identical
CSS and differ only in where the token is set:
<div style={{ "--loam-context": "primary" }}> <Button>Default brand</Button> {/* --loam-color-primary resolves at :root */}</div> <div style={{ "--loam-color-primary": "light-dark(oklch(0.62 0.2 275), oklch(0.72 0.17 275))", "--loam-context": "primary",}}> <Button>Violet brand</Button> {/* …resolves here instead */}</div>
Dark mode
Dark mode is native. Tokens are defined with CSS light-dark() and the root declares
color-scheme: light dark, so the user's OS preference is followed with no JavaScript and no
configuration; that is the default state.
The stylesheet can only speak once it has loaded. Add the matching meta tag so the browser paints the canvas in the right scheme before CSS arrives (otherwise dark-preference users get a flash of light canvas on every load):
<meta name="color-scheme" content="light dark" />To override the preference, set data-theme="dark" or data-theme="light": on the root for the
whole app, or on any element for just that subtree (the attribute simply sets color-scheme, so
every light-dark() token re-resolves there). Remove the attribute to follow the OS preference
again.
The same mechanism gives you an inverted "on-dark" section: set data-theme="dark" on the region
(or color-scheme: dark in its CSS; the attribute is just a setter for it) and every
light-dark() token flips. One caveat: colours already resolved on an ancestor inherit as
resolved values and don't re-resolve, so the inverted region must also re-declare color (e.g.
color: var(--loam-color-fg)) for descendants to pick up the flipped value.
This is one instance of LoamUI's baseline posture:
the user's stated preferences are the default. Colour scheme is followed
natively, motion exists only inside prefers-reduced-motion: no-preference, and forced
colour palettes are honoured rather than overridden. Everything beyond that baseline (a saved
theme, an animation) is an explicit opt-in layered on top.
Contexts
A context declares what a region means, as a custom property (--loam-context) that every
LoamUI component inside adopts. The mechanics (the vocabulary, one-element regions, why the
property lives on an ancestor) are the Contextualism guide's subject;
what matters for theming is that a context remaps semantic colour tokens only, so it composes
with everything on this page:
@scope (.danger-zone) to ([class*="loam-"]) { @layer loamui.components { :scope { --loam-context: danger; } }}
<section className="danger-zone"> {/* everything inside adopts the danger accent: buttons, checked states, carets: even focus rings */} <Field.Item><Field.Label><Checkbox /> I understand this is permanent</Field.Label></Field.Item> <Button>Delete</Button></section>
Theme, context, and instance are one mechanism at three scopes: remap tokens on :root
to set a brand, declare a context on a region to give it meaning, set a property on an instance to
override one control.
Most useful to override
Start here when theming:
--loam-color-primary: the brand colour (the soft tint, solid fill, focus ring and hover/active states all derive; there is nothing else to sync)--loam-color-bg,--loam-color-surface,--loam-color-fg,--loam-color-line: surfaces & text--loam-radius-md,--loam-radius-lg: corner rounding--loam-font,--loam-font-display: the body and heading font families (bothsystem-uiby default; set either and it flows through the whole stack)--loam-duration-sm/md/lg,--loam-ease: motion by intent (micro feedback, defaults, overlay enter/exit)
Fluid type & spacing
The type (--loam-text-xs…3xl) and spacing (--loam-space-3xs…xl) scales are fluid clamp()
values in container units (cqi), generated with Utopia; the calculator
parameters are committed as comments in tokens.css. Without a container they respond to the
viewport. A container affects values resolved on its descendants; it does not
recompute a font size inherited from outside. Resolve the body font token on a
content element inside the measuring region:
@scope (.sidebar) to ([class*="loam-"]) { @layer loamui.components { :scope { container-type: inline-size; } > div { font-size: var(--loam-text-md); padding: var(--loam-space-xs); } }}
The container itself measures an ancestor when it uses cqi. Put locally
responsive type and spacing on its content, and test the composition in a
plain parent without a preview wrapper.
Corner radii are deliberately not fluid: rounding shouldn't breathe. Control heights aren't tokens at all: buttons and form controls share one derived anatomy (padding + line-height + border), so they align by construction at every container width.
Because tokens cascade, you can theme per-brand or per-section by setting variables on any wrapper element; the whole theme is just values in the cascade.
Extending styles through public contracts
Theme with semantic tokens, and use the documented public custom properties for
component-specific adjustments. Inspect the component reference before using a
hook; the internal --_* variables and loam-* classes are not an application
styling API. Do not bypass the component by copying its class onto raw HTML.
For example, a wider Modal uses its public sizing property:
<Modal.Popup style={{ "--loam-modal-size": "48rem" }}> <Modal.Title>Release notes</Modal.Title> <p>Changes in this release.</p></Modal.Popup>
Compose it inside Modal.Root with a Modal.Trigger, following the
Modal reference. This is a documented geometry hook;
it does not require a new appearance prop or a selector into the popup.
Your own root class can own layout. Keep the core surface, padding and internals intact, and use a scope limit for embedded components:
<section className="plans"> <ul> <Card render={<li />}> <h2>Team plan</h2> <p>A shared workspace for your team.</p> <SignpostLink href="/plans/team">Explore the team plan</SignpostLink> </Card> </ul></section>
@scope (.plans) to ([class*="loam-"]) { @layer loamui.components { :scope { container-type: inline-size; } ul { display: block grid; gap: var(--loam-space-s); grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); list-style: none; padding-inline-start: 0; } }}
Import Card and SignpostLink from @loamui/core. The list owns layout; Card
owns its surface. There is no extra app layer that overrides private core
selectors. Establish loamui.tokens, loamui.elements, loamui.components
in that order before any styles register them, as described in
installation.