Skip to content

Timeless ships CSS in three tiers — the layer order, behavior, and the look — and only the last is optional. Loading CSS covers which is which. This page is about the last one: how to change how Timeless looks, or replace its appearance entirely.

There are 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 uiTokenGroups from @timelessui/components, so you can drive a theme editor or a token audit from the same list this page is generated from. The export names the vocabulary, not one theme’s values: Atmosphere fills these names in, and a theme of your own fills in the same ones.

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(#5c6069, #a5a7b0)
--ui-fg-subtlelight-dark(#80848d, #8e9099)
--ui-linelight-dark(#d7d9de, #343641)
--ui-line-stronglight-dark(#b8bcc5, #505360)
--ui-accentlight-dark(#0056bd, #66b0ff)
--ui-accent-hoverlight-dark(#004299, #8ec5ff)
--ui-accent-activelight-dark(#004ea9, #7db9ff)
--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, #fb8f8f)
--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-tooltip0 1px 1px rgb(0 0 0 / 12%), 0 2px 6px 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.

The theme is the optional tier. Import tokens.css and core.css and no theme at all, and every component stays positioned, structurally intact, and operable while looking like nothing in particular — which is exactly what you want when the look is yours to supply. See Loading CSS for the imports and the Tailwind caveat.

What you must keep is core. It is not a look: it is what anchors a Select surface to its trigger, keeps a closed menu off screen, and keeps a filtered option hidden. Dropping it leaves components that render but misbehave, which is worse than unstyled.

The public anatomy is the contract, and it is unchanged whether the theme is loaded or not:

  • .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 targets the same anatomy — Utility CSS and Tailwind works one through end to end:

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

You can also replace the theme wholesale rather than opt out of it. themes/atmosphere/ is one directory of plain stylesheets, one per component, plus its own tokens.css holding the token values this page documents. A second theme is a sibling of the same shape.

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