Design tokens for building new CSS in this codebase. We use Open Props — a set of CSS custom properties (sizes, colors, shadows, easing, and more; 500+ in total) composed directly in your own rules. No utility classes, no build step.
For shared component patterns, see Components.
Pages link to /ui/ui.css, which @imports the Open Props
token layer, the utility-class layer and forms.css. Token sources live under public/open-props/*.css.
/ui/ui.css provides the CSS that several components or plain markup share (.btn, .badge, cards, forms,
status animations). CSS that only one component or decorator uses lives in its module and is injected
ahead of the page's stylesheets when the component connects or the decorator first runs, so a page rule
of equal specificity still overrides it. A page that never imports x-select.mjs never parses its styles.
/index.css defines the project's semantic aliases
(--surface-*, --text-*, --brand, --border-color) and
dark-mode variants.
A few global rules apply to every page at the html / body
level (defined in /index.css). They aren't tokens, but they set the
defaults for every layout:
body { margin: 0 } — no default page margin.html, body { overscroll-behavior-y: contain } — no scroll-chaining / pull-to-refresh bounce.html { scrollbar-gutter: stable } — always reserve the scrollbar
lane so a page with a scrollbar and one without are the same
width. This stops the ~15px horizontal jump when navigating between short
(no-scrollbar) and long (scrollbar) pages. Degrades gracefully: engines
without scrollbar-gutter ignore it and use overlay scrollbars
that don't consume layout width, so there's nothing to shift.
Exception — full-bleed app shells. A page that fills the
viewport (body { height: 100dvh; overflow: hidden }) and never
scrolls at the viewport should opt out with html { scrollbar-gutter: auto }
in its page-level <style>, otherwise the reserved scrollbar space remains as an empty strip. Put the stable gutter on the page's actual document
scroll region instead. studio.html does this — it keeps a full-width
waveform edge-to-edge and reserves the gutter only on its document column.
16px or #3b82f6. A transition duration uses a literal because no duration token is loaded. Choose a value from the short list on Motion § Durations.--surface-1, --text-1, --brand) rather than raw palette values (--stone-4).--_ private props for component-internal variants.color-mix — color-mix(in oklch, var(--brand) 10%, transparent),
not #rrggbb+opacity. A mix with transparent is safe in either space,
because a fully transparent color contributes no hue. When the other color is an opaque near-neutral (var(--surface-1), var(--surface-3)) its remaining hue can produce different results across browser engines. Use in oklab — see
Radio group · color-mix.
@media (prefers-reduced-motion: no-preference) so
users who opt out get static UI; that is the form /ui/ui.css and /index.css
use. Open Props names the same query --motionOK, but that is a
@custom-media alias and this project has no build step to resolve it, so
@media (--motionOK) never matches and the motion silently never runs — the same limitation as the named breakpoints (Breakpoints). Details on
Motion.