Skip to content

Theming

Timeless has three styling surfaces, in order of how often you should reach for them:

  1. Design tokens — global values every component reads. Change these to rebrand everything.
  2. Component variables — per-component custom properties. Change these to restyle one component.
  3. Your own CSS — public classes, plain attributes, and data-ui-part selectors. Change these when you want something the first two cannot express.

There is no configuration file and no build step. All three are plain CSS.

tokens.css opens with the layer order, so every Timeless rule lands in a named layer:

@layer ui.tokens, ui.components, ui.utilities;

Unlayered CSS beats layered CSS, whatever the specificity. So your own stylesheet wins over component styles by default — a single class overrides a Timeless rule with three selectors, and you never need !important:

/* Wins over .ui-button, no matter how specific the component rule is. */
.checkout-button {
border-radius: 0;
}

Put your CSS in ui.utilities when you want it layered but still above the components, and import Timeless styles before your own so the layer order is established first.

Tokens are CSS custom properties on :root, all prefixed --ui-. Redefine any of them in your own :root block, or scope them to a subtree to theme part of a page:

:root {
--ui-accent: oklch(58% 0.19 265);
--ui-radius-control: 0.25rem;
--ui-font-sans: 'Inter Variable', system-ui, sans-serif;
}
.marketing-section {
--ui-bg-surface: #fffaf0;
}

The names and groups are exported as atmosphereTokenGroups from @timelessui/components, so you can drive a theme editor or a token audit from the same list this page is generated from.

TokenValue
--ui-bg-pagelight-dark(#f7f7f8, #111113)
--ui-bg-surfacelight-dark(#ffffff, #19191d)
--ui-bg-surface-raisedlight-dark(#f1f2f4, #24242a)
--ui-bg-surface-subtlelight-dark(#f9f9fa, #151519)
--ui-fglight-dark(#17171a, #f4f4f5)
--ui-fg-mutedlight-dark(#666a73, #a5a7b0)
--ui-fg-subtlelight-dark(#80848d, #8e9099)
--ui-linelight-dark(#d7d9de, #343641)
--ui-line-stronglight-dark(#b8bcc5, #505360)
--ui-accent#0064d8
--ui-accent-hover#0045b7
--ui-accent-active#005cd7
--ui-accent-softlight-dark(rgb(0 122 255 / 10%), rgb(0 122 255 / 18%))
--ui-successlight-dark(#0f7134, #4ade80)
--ui-success-softlight-dark(rgb(15 113 52 / 10%), rgb(74 222 128 / 14%))
--ui-warninglight-dark(#805500, #facc15)
--ui-warning-softlight-dark(rgb(128 85 0 / 12%), rgb(250 204 21 / 14%))
--ui-dangerlight-dark(#b91c1c, #f87171)
--ui-danger-softlight-dark(rgb(185 28 28 / 10%), rgb(248 113 113 / 14%))
--ui-focuscolor-mix(in oklab, var(--ui-accent), transparent 25%)
TokenValue
--ui-bg-controllight-dark(#ececef, #25262b)
--ui-bg-control-hoverlight-dark(#e1e2e6, #30323a)
--ui-bg-control-activelight-dark(#d6d7dc, #383b44)
--ui-bg-control-mutedlight-dark(#f0f1f3, #202126)
--ui-bg-accent#0064d8
--ui-bg-accent-hovercolor-mix(in oklab, #0064d8 78%, #0045b7)
--ui-bg-accent-activecolor-mix(in oklab, #0064d8 70%, #0050ad)
--ui-bg-danger#cf2034
--ui-bg-danger-hovercolor-mix(in oklab, #cf2034 82%, #a91427)
--ui-bg-danger-activecolor-mix(in oklab, #cf2034 74%, #93101f)
TokenValue
--ui-radius-xs0.25rem
--ui-radius-sm0.375rem
--ui-radius-md0.5rem
--ui-radius-lg0.625rem
--ui-radius-xl0.875rem
--ui-radius-control0.875rem
--ui-radius-pill999px
TokenValue
--ui-shadow-control0 1px 1px rgb(0 0 0 / 10%), 0 2px 5px rgb(0 0 0 / 8%), inset 0 1px 0 …
--ui-shadow-control-hover0 1px 1px rgb(0 0 0 / 10%), 0 2px 6px rgb(0 0 0 / 9%), inset 0 1px 0 …
--ui-shadow-control-activeinset 0 1px 2px rgb(0 0 0 / 16%), inset 0 1px 0 rgb(255 255 255 / 12%)
--ui-shadow-control-accent0 1px 1px rgb(0 0 0 / 10%), 0 2px 5px rgb(0 122 255 / 12%), inset 0 1…
--ui-shadow-control-danger0 1px 1px rgb(0 0 0 / 10%), 0 2px 5px rgb(255 51 71 / 14%), inset 0 1…
--ui-shadow-outline-control0 1px 1px light-dark(rgb(0 0 0 / 8%), rgb(0 0 0 / 28%)), 0 2px 5px li…
--ui-shadow-floating0 1px 1px rgb(0 0 0 / 10%), 0 8px 24px rgb(0 0 0 / 10%)
--ui-shadow-insetinset 0 1px 2px light-dark(rgb(0 0 0 / 8%), rgb(0 0 0 / 26%))
TokenValue
--ui-space-10.25rem
--ui-space-20.375rem
--ui-space-30.5rem
--ui-space-40.75rem
--ui-space-51rem
TokenValue
--ui-font-sansInter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, '…
--ui-font-mono'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, ui-monospace, m…
TokenValue
--ui-duration-fast120ms
--ui-ease-standardcubic-bezier(0.2, 0, 0, 1)
TokenValue
--ui-disabled-opacity0.56
--ui-checkerboardlinear-gradient( 45deg, var(--ui-checkerboard-ink) 25%, #0000 25% 75%…
--ui-checkerboard-inklight-dark(rgb(0 0 0 / 16%), rgb(255 255 255 / 16%))

tokens.css sets color-scheme: light dark and expresses every scheme-dependent token with light-dark():

--ui-bg-surface: light-dark(#ffffff, #19191d);

That means the platform picks the scheme and no JavaScript is involved. To force one scheme, set color-scheme on the root or on any subtree:

/* Always light, even when the OS asks for dark. */
.invoice-preview {
color-scheme: light;
}

When you override a scheme-dependent token, keep using light-dark() so both schemes stay correct:

:root {
--ui-bg-page: light-dark(#fdfdfd, #0d0d10);
}

Some tokens are deliberately scheme-independent — brand fills such as --ui-bg-accent are the same in both schemes, and their hover and active steps are derived with color-mix() in OKLab:

--ui-bg-accent-hover: color-mix(in oklab, #0064d8 78%, #0045b7);

Each component documents the custom properties it reads from its own root. They are listed in the Styling section of every component page. Set them wherever you want the change to apply:

/* Every button in the app. */
.ui-button {
--ui-button-radius: 0.375rem;
}
/* One button. */
.checkout-button {
--ui-button-bg: var(--ui-success);
--ui-button-height: 3rem;
}

Component variables are the right tool when a token would be too broad: --ui-radius-control rounds every control, while --ui-button-radius rounds only buttons.

Components stay usable with no Timeless stylesheet at all. The public anatomy is the contract:

  • .ui-* classes identify CSS component roots.
  • data-ui-* attributes carry contract-declared configuration on those roots.
  • Plain attributes configure ui-* custom-element hosts.
  • data-ui-part identifies authored anatomy.
  • Native attributes, ARIA, and platform pseudo-classes carry state.

So a utility-class or CSS-in-JS project can skip the stylesheets and target the same anatomy:

[data-ui-part~='trigger'] {
/* your own trigger styling */
}
ui-tabs [role='tab'][aria-selected='true'] {
/* your own selected-tab styling */
}

Never target data-ui-internal-*. Those are private runtime hooks and change without notice.