# Timeless UI — full documentation Every guide and every component contract. See https://timeless.build/llms.txt for the curated index, and https://timeless.build/docs/getting-started/agents/ for how these files are meant to be used. --- > Framework-agnostic UI components built on modern web standards. Most components are plain CSS over > native HTML and need no JavaScript; the rest are Light-DOM custom elements, used only where > keyboard coordination, focus management, or state synchronisation cannot be expressed accessibly > in CSS. Targets Baseline 2025 browsers. Usable from plain HTML, React, Preact, Vue, Svelte, Solid, > or Astro. ## How to author Timeless markup Read this before writing any Timeless markup. The API is not prop-based, and guessing from React or Tailwind conventions produces markup that does not work. ### The two kinds of component Everything in the library is one of two things, and they are configured differently. Getting this wrong is the single most common failure. **CSS components** are a native element carrying a `ui-*` class. Configure them with `data-ui-*` attributes. There is nothing to register and nothing to import beyond the stylesheet. ```html ``` **Custom elements** are a registered `ui-*` tag wrapping your own markup. Configure them with plain attributes, never `data-ui-*`. Register each element you use. ```html ``` So: - `` is wrong twice — there is no `ui-button` element, and `variant` is not how a CSS component is configured. It is `
Popover content
``` ## SSR and hydration Astro renders the complete Light DOM on the server. A custom element does not need an Astro client directive. The processed script registers it in the browser, then native custom-element upgrade enhances the existing DOM. Class-only imports are SSR safe. ## Attributes, properties, events, and TypeScript Use plain attributes for authored state. Assign live properties from a client script after selecting the element. Listen for namespaced events with `addEventListener`, and import element or event detail types from the matching package entrypoint when the script uses TypeScript. Astro templates are HTML, not JSX, so no JSX declaration applies to them. Completion and hover documentation for `ui-*` tags, their attributes, and their permitted values come from the shipped editor data instead — see [Editor setup](/docs/getting-started/editor-setup/). If a page uses a React, Preact, or Solid island, import that framework's declarations for the island's `.tsx` files. CSS-only components are a root class plus `data-ui-*` on a native tag. Astro spreads attributes, so the typed helper works directly in a template: ```astro --- import { uiAttributes } from '@timelessui/components/attributes' --- ``` --- # Preact Source: https://timeless.build/docs/frameworks/preact.md Use custom elements directly with opt-in JSX types and no runtime wrapper. ## Install and TypeScript ```sh pnpm add @timelessui/components ``` Preact passes unknown props straight through to the DOM and registers any `on*` prop as an event listener, so Timeless elements work without a wrapper on any recent version. Import the JSX declarations once in your application types: ```ts import '@timelessui/components/preact' ``` The declaration is types-only. It adds no runtime code, no Preact dependency, and no peer dependency. With it imported, `ui-*` tags are known intrinsic elements and their attributes complete and type-check: ```tsx ``` `orientation` accepts only `horizontal` or `vertical`, and `activation` only `automatic` or `manual`, because both are generated from the same declaration the stylesheets are proven against. If you use `preact/compat` to run React libraries alongside Preact, import the React declarations instead — `@timelessui/components/react` — because `preact/compat` aliases the `react` module that those declarations augment. ## CSS and registration Import CSS and registration from a client entrypoint: ```ts import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/tabs.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/tabs.css' import '@timelessui/components/register/ui-tabs' ``` CSS, class, and `define/*` imports are safe during server rendering. Keep `register/*` imports in code the browser loads, because that is where they call `customElements.define` — on the server they are inert, so a server-only import registers nothing. ## Markup and hydration Use the element directly. The markup you author is the markup that ships: ```tsx export function ProjectTabs() { return (
Project details
Recent activity
) } ``` Preact hydrates the authored Light DOM, then the browser upgrades the custom element without replacing that anatomy. Attributes carry the authored default; assign DOM properties for live state after the element upgrades. ## Events Preact turns an `on*` prop into an `addEventListener` call, so namespaced Timeless events bind directly and arrive typed with the detail the element actually dispatches: ```tsx { // event.detail is TabsChangeDetail console.log(event.detail.value, event.detail.reason) }} /> ``` `onui-before-change` is cancelable: call `event.preventDefault()` to reject the transition and keep the current value. Detail types are also importable from `@timelessui/components/events` and from each element's own entrypoint. ## CSS-only components Most Timeless components are plain CSS over native HTML and have no custom element at all. Those are a root class plus `data-ui-*` attributes, which JSX cannot check per element without loosening every element in your app. Use the typed helper instead: ```tsx import { uiAttributes } from '@timelessui/components/attributes' export function PublishButton() { return ( ) } ``` See [Editor setup](/docs/getting-started/editor-setup/) for why that gap exists and what completes in each editor. --- # React Source: https://timeless.build/docs/frameworks/react.md Use custom elements directly with opt-in JSX types and no runtime wrapper. ## Install and TypeScript ```sh pnpm add @timelessui/components ``` React 19 supports custom-element properties on the client and primitive attributes during server rendering. Import Timeless JSX declarations once in your application types: ```ts import '@timelessui/components/react' ``` The declaration is types-only. Timeless does not add a runtime wrapper or React dependency. ## CSS and registration Import CSS and registration from a client entrypoint: ```ts import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/listbox.css' import '@timelessui/components/css/core/options.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/listbox.css' import '@timelessui/components/css/themes/atmosphere/options.css' import '@timelessui/components/register/ui-listbox' ``` Load definition imports from a client entrypoint. CSS and class-only imports are safe during server rendering. ## Markup and hydration Use the element directly: ```tsx export function StatusPicker() { return (
Draft
Ready
) } ``` Primitive values render as attributes on the server. Assign object or array properties after the element upgrades when a component exposes them. React hydrates the authored Light DOM, then the browser upgrades the custom element without replacing that anatomy. ## Events Attach namespaced custom events with a ref and `addEventListener` when an application needs typed, cancelable transition handling. `onui-change` and `onui-before-change` props are also declared and carry the detail type the element actually dispatches, so a `ui-tabs` handler receives a `CustomEvent` rather than a generic one. Detail types import from `@timelessui/components/events` and from each element's own entrypoint. `on*` props on custom elements and non-string attribute values both require React 19. On React 18 the declarations still type your markup, but pass primitives as attributes and bind events with a ref. ## CSS-only components Most Timeless components are plain CSS over native HTML and have no custom element at all. Those are a root class plus `data-ui-*` on a native tag, and JSX cannot check that per element without loosening every element in your app. Use the typed helper: ```tsx import { uiAttributes } from '@timelessui/components/attributes' export function PublishButton() { return ( ) } ``` `variant` accepts only the seven button variants, and Card's `filled` is a type error on a Button. See [Editor setup](/docs/getting-started/editor-setup/) for what each editor completes. --- # Solid Source: https://timeless.build/docs/frameworks/solid.md Render Timeless custom elements directly from Solid. ## Install and TypeScript ```sh pnpm add @timelessui/components ``` Import the Solid declarations once in your application types: ```ts import '@timelessui/components/solid' ``` They extend Solid's `JSX.IntrinsicElements`, so every `ui-*` tag and every permitted attribute value type-checks. Both event spellings are declared: `on:ui-change`, the namespaced form Solid recommends for custom events, and `onui-change`. The declaration is types-only and adds no runtime code and no Solid dependency. ## CSS and registration Import CSS and the required definitions once: ```ts import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/choice-groups.css' import '@timelessui/components/css/core/forms.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/forms.css' import '@timelessui/components/css/themes/atmosphere/choice-groups.css' import '@timelessui/components/register/ui-radio-group' ``` ## Markup Solid forwards custom-element attributes and properties without a Timeless adapter: ```tsx ``` ## SSR, hydration, events, and TypeScript SSR emits the authored attributes and Light DOM. Keep registration in client code so custom-element upgrade happens after hydration. Bind namespaced events in JSX, and use a ref when you need to assign live properties: ```tsx setTheme(event.detail.value)} > {/* radios */} ``` The handler receives a `CustomEvent`. Public classes and detail types are available from the granular entrypoints. CSS-only components are a root class plus `data-ui-*` on a native tag. Spread the typed helper rather than writing those attributes by hand: ```tsx import { uiAttributes } from '@timelessui/components/attributes' export function PublishButton() { return ( ) } ``` --- # Svelte Source: https://timeless.build/docs/frameworks/svelte.md Use Timeless elements as native custom elements in Svelte. ## Install and TypeScript ```sh pnpm add @timelessui/components ``` Import the Svelte declarations once in your application types: ```ts import '@timelessui/components/svelte' ``` They extend `svelteHTML.IntrinsicElements`, so `svelte-check` knows every `ui-*` tag and the values each attribute accepts. Both event spellings are declared — Svelte 5 `onui-change` and Svelte 4 `on:ui-change` — so the same declaration checks either version. It is types-only and adds no runtime code and no Svelte dependency. ## CSS and registration Import CSS and definitions from a client module or layout: ```ts import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/tabs.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/tabs.css' import '@timelessui/components/register/ui-tabs' ``` ## Markup Author the element normally: ```svelte
Project details
Recent activity
``` ## SSR, hydration, properties, and events SSR emits the complete Light DOM anatomy before the definition loads. Plain attributes express authored defaults. Bind namespaced events directly, and use `bind:this` when you need to assign live properties: ```svelte console.log(event.detail.value)}> ``` The handler receives a `CustomEvent`, because each element declares the detail type it dispatches rather than sharing one generic signature. Element classes and detail types import into TypeScript without registering anything on the server. CSS-only components are a root class plus `data-ui-*` on a native tag, which Svelte cannot check per element. Spread the typed helper instead: ```svelte ``` --- # Vanilla Source: https://timeless.build/docs/frameworks/vanilla.md Use Timeless directly from HTML and JavaScript. ## Install ```sh pnpm add @timelessui/components ``` ## CSS and registration Import styles and definitions from an application module: ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/button.css' import '@timelessui/components/css/core/toggle.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/button.css' import '@timelessui/components/css/themes/atmosphere/toggle.css' import '@timelessui/components/register/ui-toggle-group' ``` Then author normal HTML: ```html ``` The same markup is emitted by a server or copied into a static HTML document. The complete Light DOM is readable before registration, then the explicit definition upgrades it in the browser. ## Attributes, properties, and events Properties are useful for live state. Attributes provide authored defaults and reset state: ```js const group = document.querySelector('ui-toggle-group') group.addEventListener('ui-before-change', (event) => { if (!canChange(event.detail.value)) event.preventDefault() }) group.addEventListener('ui-change', (event) => save(event.detail.value)) ``` Each element declares the detail type it dispatches, so `event.detail` is typed per element rather than shared: a `ui-toggle-group` change carries `ToggleGroupChangeDetail`. No hydration runtime is required. ## Editors and TypeScript There is no JSX layer here, so nothing type-checks your HTML by default. Two things close that gap: - Register the shipped editor data once, and `ui-*` tags, their attributes, and their permitted values complete in any `.html` file. See [Editor setup](/docs/getting-started/editor-setup/). - For markup assembled in TypeScript, `@timelessui/components/attributes` gives CSS-only components a typed surface, and `@timelessui/components/validate` reports authored values that no contract permits: ```ts import { uiAttributeString } from '@timelessui/components/attributes' const markup = `` if (import.meta.env.DEV) { const { validateTimelessMarkup } = await import('@timelessui/components/validate') validateTimelessMarkup() } ``` The validator walks the document and warns about a `data-ui-*` attribute no contract declares or a value outside its permitted set — the class of typo that renders without complaint. --- # Vue Source: https://timeless.build/docs/frameworks/vue.md Configure Vue to treat ui elements as native custom elements. ## Install and compiler configuration ```sh pnpm add @timelessui/components ``` Tell the Vue compiler that `ui-` tags are custom elements: ```ts // vite.config.ts vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('ui-') } } }) ``` Then import the Vue declarations once in your application types: ```ts import '@timelessui/components/vue' ``` They register every `ui-*` tag on `GlobalComponents`, so `vue-tsc` checks attributes and their permitted values in templates. Vue camelizes a dashed event name, so `ui-change` binds as `@ui-change` in a template and is typed as `onUiChange`. The declaration is types-only and adds no runtime code and no Vue dependency. ## CSS and registration Import granular CSS and registration from a client entrypoint: ```ts import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/listbox.css' import '@timelessui/components/css/core/options.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/listbox.css' import '@timelessui/components/css/themes/atmosphere/options.css' import '@timelessui/components/register/ui-listbox' ``` ## Markup and state Use attributes for authored defaults and DOM properties for live state: ```vue
Draft
Ready
``` ## SSR, hydration, events, and TypeScript Vue can hydrate the server-rendered Light DOM before the element definition upgrades it. Keep registration in browser code. Use a typed template ref to assign properties, and bind namespaced events in the template: ```vue
Draft
``` Each element's events carry the detail type it actually dispatches, so `onChange` receives a `CustomEvent`. Public element classes and detail types import without registering anything on the server. For CSS-only components, which are a root class plus `data-ui-*` on a native tag, bind the typed helper instead of writing the attributes by hand: ```vue ``` --- # AI agents Source: https://timeless.build/docs/getting-started/agents.md Markdown routes, llms.txt, and a packaged skill, so a coding agent authors Timeless markup correctly instead of guessing from React conventions. import { Aside } from '@astrojs/starlight/components' import AgentSurfaceSizes from '../../../../components/docs/AgentSurfaceSizes.astro' import AgentsBlock from '../../../../components/docs/AgentsBlock.astro' Timeless is not prop-based. A model that has read a lot of React will reach for ``, and that is not a component — it is ` ``` No editor completes that precisely, and the reason is structural rather than a missing feature. Editor data and JSX both key completion off the tag name. The tag here is `button`, so the only hook available is a global attribute — one that applies to every element in the document and merges the values of every component sharing that attribute name. Declaring `data-ui-variant` globally would offer Card's `filled` inside a Button and Button's `ghost` inside an Alert. That is worse than offering nothing, so Timeless does not declare it. What is offered instead: - **Types.** `@timelessui/components/attributes` moves that surface into the type system: ```ts import { uiAttributes, uiAttributeString } from '@timelessui/components/attributes' uiAttributes('button', { variant: 'primary', size: 'lg' }) // { class: 'ui-button', 'data-ui-variant': 'primary', 'data-ui-size': 'lg' } uiAttributeString('alert', { variant: 'danger' }) // class="ui-alert" data-ui-variant="danger" ``` The keys are the component's attributes and the values are its permitted set, so `uiAttributes('card', { size: 'md' })` and `uiAttributes('button', { variant: 'nope' })` are both type errors. `uiAttributeString` omits values that equal the contract default, because the default is the stylesheet's base rule and needs no attribute. - **A runtime check.** `@timelessui/components/validate` walks real DOM and reports what neither the editor nor the stylesheet can: ```ts if (import.meta.env.DEV) { const { validateTimelessMarkup } = await import('@timelessui/components/validate') validateTimelessMarkup() } ``` It warns on a `data-ui-*` attribute the component does not declare, a value outside its permitted set, a value on a presence-based boolean, and `data-ui-*` used as configuration on a `ui-*` host, where it is never correct. Both entrypoints are opt-in and neither is reachable from the default import. ## Keeping the data current The editor data is regenerated and verified when the package is built, in the build, so it cannot drift from the component registry. Reload your editor window after upgrading the package if completions look stale. --- # Installation Source: https://timeless.build/docs/getting-started/installation.md Install Timeless, load its CSS, and register the elements a route uses. ```bash pnpm add @timelessui/components ``` That is the only package you install. There are no peer dependencies, no framework runtime, and no configuration file. ## Load the CSS Import the tokens plus the components a route renders: ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/button.css' import '@timelessui/components/css/core/floating.css' import '@timelessui/components/css/core/popover.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/button.css' import '@timelessui/components/css/themes/atmosphere/popover.css' ``` Import `tokens.css` first — it declares the cascade-layer order that lets your own CSS override component styles. For prototypes, `@timelessui/components/css/themes/atmosphere.css` loads everything in one request. See [Loading CSS](/docs/styling/css/) for the trade-off and [Theming](/docs/styling/theming/) for what you can override. ## Register the elements you use CSS-only primitives such as Button, Alert, and Card need no JavaScript at all. Enhanced components are custom elements, and registration is explicit — per element, never automatic: ```js import '@timelessui/components/register/ui-popover' ``` `register/` is a side-effect import. It defines the element as the module evaluates, so it has to run in code the browser actually loads — an import that only ever executes on the server registers nothing, and the element never upgrades. `@timelessui/components/register` registers every element in one import, which is convenient in a prototype and ships every component's behavior. ### Registering it yourself The same registration is also a function, on the matching `define/` path: ```js import { definePopoverElement } from '@timelessui/components/define/ui-popover' definePopoverElement() ``` Reach for this when you need to control _when_ registration happens — after a feature check, inside an `onMount`, behind a dynamic import — or to define into a window other than the current one, which the optional `targetWindow` argument is for. `define/` has no side effect of its own: importing it and calling nothing registers nothing. Importing `@timelessui/components/popover` gives you the class and helpers without registering anything either, which keeps that import safe during server rendering. Your framework's guide covers when the upgrade happens relative to hydration — see [React](/docs/frameworks/react/), [Vue](/docs/frameworks/vue/), [Svelte](/docs/frameworks/svelte/), or [Solid](/docs/frameworks/solid/) — and [Packages and entrypoints](/docs/reference/packages/) lists every import. ## Next Build something with it in [Quick start](/docs/getting-started/quick-start/), or jump straight to a component such as [Button](/docs/components/button/) or [Dialog](/docs/components/dialog/). --- # The post-framework era Source: https://timeless.build/docs/getting-started/post-framework.md Why UI primitives outlive the frameworks they were written for, which browser features made that possible, and how Timeless differs from the other framework-agnostic libraries. import { Aside } from '@astrojs/starlight/components' The frontend ecosystem rearranges itself every few years. Your buttons, dialogs, and dropdowns do not need to. The problem is that most component libraries make them, because the primitives are coupled to a framework rather than to the platform underneath it. This page is the reasoning behind the library. It is not required reading before [Installation](/docs/getting-started/installation/) — but it explains why the markup on every component page looks the way it does. ## The cost is transferability, not bundle size A component library written for one framework is only reusable inside that framework. shadcn/ui is React; using the same primitives in Vue or Svelte meant the community re-implementing them as separate projects rather than importing them. That is the coupling made visible: the design work transferred, the code could not. So the cost of switching frameworks is not only the application. It is the layer underneath it — the layer that had nothing framework-specific about it in the first place. A dialog needs a top layer, a focus trap, Escape handling, and an accessible name. None of those are React opinions. Timeless keeps that layer in the two languages every framework already renders: HTML and CSS. A custom element is an HTML tag, so there is no adapter to write and no boundary to cross. The [framework guides](/docs/frameworks/vanilla/) are about where the imports go, not about a different implementation per framework. ## Why this became possible Frameworks did not invent overlays and dark mode for fun. They shipped those features because the platform had no version of them. Each row below was a genuine gap, and each has since closed: | Once needed framework or library code | Now a platform feature | | ------------------------------------------------ | ----------------------------------------- | | Positioning a surface next to its trigger | CSS anchor positioning | | Top layer, light dismiss, Escape, focus trapping | Popover API and `` | | Dark mode through a theme provider and a class | `light-dark()` and `color-scheme` | | Winning a specificity fight with library CSS | Cascade layers | | Component state a stylesheet can read | `ElementInternals` states and `:state()` | | A click handler to open a dialog | Invoker Commands (`command`/`commandfor`) | | A reusable tag that carries its own behavior | Custom elements | These were not framework ideas. They were framework workarounds, and a workaround outlives its reason quietly. The exact versions each feature landed in, and what happens in a browser missing one, are in [Browser support](/docs/reference/browser-support/). ## How the library uses that There are 49 documented components, and they divide by whether the platform can express them declaratively. **26 are CSS over native HTML, with no JavaScript at all.** A class on real markup. There is no runtime to boot and no hydration step, because there is nothing to hydrate — it is a stylesheet. **23 are custom elements**, used where keyboard coordination, focus management, or ARIA relationships genuinely cannot be expressed in CSS. Those elements enhance markup you already wrote; they do not render it. Popover is the pattern all of them follow: ```html ``` That opens, closes, light-dismisses, and handles Escape with JavaScript disabled, because `popovertarget` and `popover` are platform attributes. Registration then adds only what the platform does not: `aria-controls`, `aria-expanded`, a default `aria-haspopup`, a surface `role`, and anchored positioning. Nothing is created, so nothing can arrive late and rearrange the page. The markup you author is the markup that ships. That is what removes the unstyled flash and the shell that shifts once a bundle lands — there was never a moment when the DOM was wrong. ## What is different from the other framework-agnostic libraries Framework-agnostic UI is not a new idea, and pretending otherwise would be dishonest — open-wc keeps [a list of component libraries](https://open-wc.org/guides/community/component-libraries/) built on the same standards. Several are excellent. Timeless makes three different trades. **CSS-first rather than JavaScript-first.** In most web-component libraries the component renders from JavaScript, so the element is empty until its bundle executes. Here more than half the library has no JavaScript to execute, and the rest enhances existing markup instead of producing it. **Light DOM rather than Shadow DOM.** Shadow DOM gives real style encapsulation, which is a legitimate thing to want. It also means your stylesheet cannot reach inside, so styling happens through whatever custom properties and `::part()` hooks the author remembered to expose. Timeless keeps anatomy in the Light DOM as a `data-ui-part` token list, so your CSS — or your utility classes — select it directly. Cascade layers put your rules above ours without a specificity fight. **Server-rendered output that is already correct.** Declarative Shadow DOM has made SSR workable for shadow-based libraries, so this is a narrowing gap rather than an absent capability. The distinction that remains is what the first paint is worth: Timeless emits plain HTML and CSS that is styled and operable before any script runs, rather than markup awaiting an upgrade. The trade is deliberate. You give up style encapsulation, and you get markup you can inspect, override, and server-render with no framework in the request path. ## What post-framework does not mean It does not mean less rigor. It means the rigor moves out of a runtime and into the build. Nothing here re-implements what the browser already does, so the discipline is in keeping the boundaries honest. Every public attribute, part, state, and permitted value is declared once in a component registry, and the build proves those values against the stylesheets in both directions — a documented value is a value the CSS implements. The CSS ships in three tiers, and a script fails the build if a `core/` stylesheet declares a colour or a theme stylesheet keeps a property core owns. [Styling](/docs/styling/css/) covers what that split means when you replace the theme. You still own accessibility, and so do we: the conformance target is WCAG 2.2 Level AA, every interactive component declares its keyboard contract, and the end-to-end suite asserts each component still reads correctly with the theme removed. Frameworks were a loan taken against an unfinished platform. The platform shipped. This is what it looks like to stop making payments — not less engineering, just less of it spent in your users' browsers. ## Next - [Installation](/docs/getting-started/installation/) — add the package, load the CSS, register an element. - [Quick start](/docs/getting-started/quick-start/) — build an interface from native HTML and explicit enhancement. - [What Timeless does not ship](/docs/reference/scope/) — where the boundary of "primitive" sits, per component. --- # Quick start Source: https://timeless.build/docs/getting-started/quick-start.md Build a Timeless interface from native HTML and explicit enhancement. Import CSS and the definition entrypoint: ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/button.css' import '@timelessui/components/css/core/floating.css' import '@timelessui/components/css/core/popover.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/button.css' import '@timelessui/components/css/themes/atmosphere/popover.css' import '@timelessui/components/register/ui-popover' ``` Author the anatomy in HTML. This is the minimum a Popover needs: ```html

Ready to publish

All required checks passed.

``` `popovertarget` is what makes this work before JavaScript: the button opens the surface natively, and the browser handles light dismiss and Escape. Registration then adds `aria-controls`, `aria-expanded`, `aria-haspopup`, the surface `role`, and anchored positioning. ## Author the parts you own Timeless never writes your content or your accessible names. When the surface is a dialog, name and describe it yourself: ```html ``` The component pages show this fuller form, because an example should be correct on its own. The **Attributes** and **Anatomy** tables on each page mark what you must author and what the element adds for you. --- # Browser support Source: https://timeless.build/docs/reference/browser-support.md The platform features Timeless requires, and what happens where they are missing. Timeless targets Baseline 2025 browsers. The floor is the newest requirement in each engine, and two features set it between them — `:state()` in Chrome and Firefox, `light-dark()` in Safari: **Chrome and Edge 125, Safari 17.5, and Firefox 126.** There are no polyfills, so support tracks the platform features below. Versions are taken from [MDN browser-compat-data](https://github.com/mdn/browser-compat-data) and record the first release of each feature, not a support promise. Check [Baseline](https://web.dev/baseline) for current data. ## Required features All Baseline, and all older than the floor above except the last two rows, which set it. A browser missing any of these cannot render Timeless correctly. | Feature | Chrome / Edge | Safari | Firefox | Used by | | ------------------------ | ------------- | ------ | ------- | --------------------------------------------------------- | | Custom elements | 54 | 10.1 | 63 | Every enhanced component | | `` | 37 | 15.4 | 98 | Dialog, Sheet | | Cascade layers | 99 | 15.4 | 97 | Every stylesheet | | `:has()` | 105 | 15.4 | 121 | Field, Choice, and Range layout | | `color-mix()` | 111 | 16.2 | 113 | Derived accent and danger fills | | Popover API | 114 | 17 | 125 | Popover, Hover Card, Menu, Context Menu, Select, Combobox | | `light-dark()` | 123 | 17.5 | 120 | Tokens and themed surfaces | | `:state()` custom states | 125 | 17.4 | 126 | Sheet drag, Toast exit, Color Picker, and Copy Button | ## Progressive features All three are Baseline as well, but they landed much later than the floor, so a browser that is current-but-not-latest may still be missing them. Each degrades to something usable. | Feature | Chrome / Edge | Safari | Firefox | Without it | | -------------------------------------- | ------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `@starting-style` and `allow-discrete` | 117 | 17.5 | 129 | Overlays appear and disappear without the enter and exit transition. | | CSS anchor positioning | 125 | 26 | 147 | Overlay surfaces stay in normal flow next to their trigger instead of being anchored to it. They still open, close, dismiss, and trap focus correctly. | | Invoker Commands | 135 | 26.2 | 144 | A dialog or modal sheet trigger needs JavaScript to open its panel, because the component falls back to its click listener. Popovers are unaffected: they use `popovertarget`. | One requirement is not a version at all. [Copy Button](/docs/components/copy-button/) needs `navigator.clipboard`, which every target engine has shipped for years but which is undefined outside a secure context and refuses the write without transient user activation. So the same build works on `https://` and on `localhost` and does nothing over plain `http://` — a staging box, or a LAN address during development. The component reports that as `unsupported` on `ui-copy` rather than failing silently, and a trigger authored `hidden` is revealed only once the API is there. Anchor positioning is the widest gap in practice: Firefox only shipped it in 147, well above the Firefox 126 floor. It is a layout refinement rather than a functional requirement, so a popover in an older Firefox is fully usable, just not tethered to its trigger. Invoker Commands are the one place where the markup you author decides which path runs. Put `command="show-modal"` and `commandfor` on a Dialog or modal Sheet trigger and the browser opens the panel itself, before the bundle loads; add `command="close"` to a close button and the platform closes it and copies the button's `value` into `returnValue`. Timeless reads those attributes and stands down — it never writes them, since an attribute added during enhancement would be back to needing JavaScript. Where the API is missing, the component's click listener does the same work, so one set of markup is correct everywhere. Two limits come from the platform rather than from Timeless. An invoker can only name an `id` that exists in the markup, so an invoked `` needs an explicit one rather than a generated one. And there is no built-in command for `dialog.show()`, so a **non-modal** Sheet cannot be opened declaratively; leave `command` off that trigger and the click listener opens it. Its close buttons still work declaratively. ### Wide-gamut color `oklch()`, `oklab()`, `lch()`, `lab()`, `hwb()`, and `color(display-p3 …)` are Baseline everywhere Timeless runs — Chrome 111, Safari 15, Firefox 113 — so this is not a browser-support question. What varies is the **display**: an out-of-gamut color is rendered clamped on an sRGB screen. Color Picker parses and edits the value faithfully regardless, flags when a color falls outside the current gamut, and offers an explicit clamp control so you can decide rather than guess. ## Unsupported capability When a component needs a platform capability the browser lacks, enhancement stops and the authored markup is left exactly as written. Nothing is hidden, no public diagnostic attribute is added, and no polyfill is loaded. A Popover in a browser without the Popover API keeps its native trigger and content; the surface simply does not become a top-layer popover. This is why every component page documents markup that already works before JavaScript runs. Two components are the exception, and both are platform gaps rather than support ones. [Context Menu](/docs/components/context-menu/) has no declarative way to open a surface at pointer coordinates, so with scripting off the browser shows its own context menu and the authored `ui-menu` stays hidden. Nothing reachable only from a context menu is reachable at all, in any browser, until the bundle loads. [Copy Button](/docs/components/copy-button/) has no declarative way to reach the clipboard: `navigator.clipboard` is undefined outside a secure context and refuses the write without a user gesture, and `document.execCommand('copy')` is deprecated. Author its trigger `hidden` and registration reveals it once the API is there, so no dead control renders. [Sheet](/docs/components/sheet/)'s swipe-to-dismiss is a gentler version of the same shape: a gesture is an addition, so Escape and the close control stay, and the sheet is fully operable without a pointer. ## What is tested Automated coverage runs on every change: - Chromium for the website and the component catalog. - Firefox and WebKit for platform contracts: form association, native validation, dialog and popover behavior, and no-JavaScript rendering. - axe WCAG 2.2 A and AA rules, plus reflow and text-spacing checks, on every documented route. These are regression evidence, not a conformance claim. Keyboard, zoom, forced-colors, and screen-reader review with real assistive technology still belong to your release process. --- # Packages and entrypoints Source: https://timeless.build/docs/reference/packages.md Every published import, what it contains, and whether it is safe on the server. ## Packages | Package | Role | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@timelessui/components` | Everything a consumer needs to build an interface: CSS, custom elements, contracts, and utilities. This is the only package you install for components. | | `@timelessui/color` | CSS color parsing, conversion, gamut, and contrast utilities. It depends on nothing, and arrives as a dependency of the components; install it directly only if you want the color math without them. | | `@timelessui/core` | The internal custom-element authoring layer the components are built on. It arrives as a dependency; you do not import it directly, and it is not part of the public API. | ```bash pnpm add @timelessui/components ``` ## Entrypoints | Import | Contains | Side effects | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------- | | `@timelessui/components` | Contracts, design-token names, permitted attribute values, event types, and every element class. | None | | `@timelessui/components/{element}` | One enhanced element's class and helpers, for example `@timelessui/components/dialog`. | None | | `@timelessui/components/register/ui-{element}` | Registers one custom element as it is imported. | Calls `customElements.define` | | `@timelessui/components/register` | Registers every public custom element as it is imported. | Calls `customElements.define` | | `@timelessui/components/define/ui-{element}` | One `define{Element}Element()` function. Registers nothing until you call it. | None | | `@timelessui/components/define` | Every define function, plus `defineTimelessElements()`. Registers nothing until you call it. | None | | `@timelessui/components/css/tokens.css` | The cascade-layer order and `color-scheme`. Required, and imported first. | Stylesheet | | `@timelessui/components/css/core/{component}.css` | One component's behavior: anchoring, box participation, scrolling, native resets. Required. | Stylesheet | | `@timelessui/components/css/core.css` | Every `core/` file. Required, and the tier you keep when you replace the theme. | Stylesheet | | `@timelessui/components/css/themes/atmosphere/tokens.css` | Every `--ui-*` value. Required by any Atmosphere component file, which carries no fallbacks. | Stylesheet | | `@timelessui/components/css/themes/atmosphere/{component}.css` | One component's look. Optional — this is the tier you replace. | Stylesheet | | `@timelessui/components/css/themes/atmosphere.css` | The required tiers plus every Atmosphere stylesheet. The one import a prototype needs. | Stylesheet | | `@timelessui/components/collection` | Locale-aware matching and disabled-aware keyboard navigation. | None | | `@timelessui/color` | Color parsing, serialization, conversion, gamut, and WCAG contrast. Its own package. | None | | `@timelessui/components/events` | Transition event and detail types. | None | | `@timelessui/components/value-state` | Authored-default and live-value helpers. | None | | `@timelessui/components/attributes` | Typed attribute builder for CSS-only components. | None | | `@timelessui/components/validate` | Development-time check that authored markup matches the contracts. | None | | `@timelessui/components/react` | React 19 intrinsic-element declarations. Types only. | None | | `@timelessui/components/preact` | Preact intrinsic-element declarations. Types only. | None | | `@timelessui/components/solid` | Solid intrinsic-element declarations. Types only. | None | | `@timelessui/components/vue` | Vue `GlobalComponents` declarations. Types only. | None | | `@timelessui/components/svelte` | Svelte `svelteHTML.IntrinsicElements` declarations. Types only. | None | Only enhanced elements have a class entrypoint. CSS-only primitives such as Button, Alert, and Card are classes and attributes rather than JavaScript, so they have no per-component module at all — their permitted values live on the root import instead. Two pairs share one module behind two subpaths, so importing either name gives you both classes: `radio-group` and `checkbox-group` are both `choice-group`, and `toaster` and `toast` are both `toast`. ## What an entrypoint weighs Enhanced elements are composed, so an entrypoint pulls the modules it is built on. Select contains Listbox, the option layer, Popover, and the anchoring layer, which is why it is the heaviest single import in the package. Bundled and minified, `@timelessui/components/select` is about 13.5 kB gzipped. Adding `@timelessui/components/combobox` to the same page brings the pair to about 16 kB rather than to the sum of the two, because everything under both of them is shared. Six enhanced elements — Select, Combobox, Listbox, Popover, Menu, and Dialog — come to about 21 kB together. There is no framework runtime underneath any of it, and a CSS-only component such as Button or Card adds no JavaScript at all. Those are measurements rather than budgets, taken with esbuild at default settings; your bundler will land somewhere nearby. The package ships unminified so your own pipeline decides. ## Contracts and permitted values The root import is the machine-readable version of every component reference page: ```ts import { buttonVariants, componentContracts } from '@timelessui/components' buttonVariants // ['primary', 'secondary', 'outline', 'ghost', 'danger', 'danger-outline', 'link'] componentContracts.tabs.parts.filter((part) => part.required).map((part) => part.selector) // ["[role='tablist']", "[role='tab']", "[role='tabpanel']"] ``` `componentContracts` records each component's root, stylesheets, attributes with their permitted values and defaults, authored parts, public state, and events. Every attribute that takes a fixed set of values also names the array that set is exported as, in its `set` field. The build proves those values against the stylesheets in both directions: a value a stylesheet selects must be declared, and a declared value must be selected or be the default, which is the base rule and so has no selector of its own. `pnpm -F @timelessui/components run contracts:validate` fails either way, which is why the reference tables cannot drift from the CSS. Permitted values are also exported individually as `as const` arrays with matching union types, each declared exactly once and generated from the same source as the contracts: `buttonVariants`, `buttonSizes`, `alertVariants`, `spinnerVariants`, `badgeVariants`, `avatarShapes`, `avatarStatuses`, `cardVariants`, `linkVariants`, `listVariants`, `separatorVariants`, `separatorOrientations`, `skeletonShapes`, `skeletonWidths`, `groupOrientations`, `primitiveSizes`, `primitiveDensities`, `compactDensities`, `tableAlignments`, `breadcrumbSeparators`, `formControlSizes`, `fieldLayouts`, `formDensities`, `choiceGroupOrientations`, `floatingPlacements`, `tabsOrientations`, `tabsActivations`, `dialogKinds`, `sheetPositions`, `popoverRoles`, `hoverCardVariants`, `menuOrientations`, `toolbarOrientations`, `collectionAlignments`, `optionFilterModes`, `toasterPlacements`, `toasterStacks`, `toggleGroupOrientations`, `toggleGroupSelections`, and `colorPickerFormats`. Arrays with identical values keep separate names because they are separate public exports. `buttonSizes`, `primitiveSizes`, and `formControlSizes` are all `sm | md | lg`, and each names the set its own components implement. ## Design tokens `uiTokenGroups` names every public custom property, grouped by purpose, and `isUIToken` narrows a string against the set. Both name the theme-neutral `--ui-*` vocabulary rather than Atmosphere, which is one set of values for it. See [Theming](/docs/styling/theming/) for the values and how to override them. ## Custom Elements Manifest `@timelessui/components/custom-elements.json` is exposed through the package's `customElements` field. It declares attributes with union types and defaults, reflecting properties under their real names, events with the detail type each element actually dispatches, CSS custom properties, and custom states — for editors, generators, and downstream tooling. Authored parts appear under a namespaced `timeless:parts` key rather than the manifest's `cssParts`, each with the selector it is addressed by. `cssParts` describes `::part()`, which only crosses a shadow boundary; Timeless anatomy is Light DOM you author yourself, so `[data-ui-part~='trigger']` is the real contract and claiming a shadow one would be wrong. ## Framework typings Every framework typing is generated from the manifest and is types-only: importing one adds no runtime code, no wrapper, and no dependency on that framework. Import the one you need once in your application types. | Import | Augments | | ------------------------------- | --------------------------------------- | | `@timelessui/components/react` | `react` and `react/jsx-runtime` `JSX` | | `@timelessui/components/preact` | `preact` and `preact/jsx-runtime` `JSX` | | `@timelessui/components/solid` | `solid-js` `JSX` | | `@timelessui/components/vue` | `@vue/runtime-dom` `GlobalComponents` | | `@timelessui/components/svelte` | `svelteHTML.IntrinsicElements` | Each declares, per element, the attributes an author writes typed to their permitted values, the DOM properties those attributes reflect under the element's real property names, and the element's own events with its own detail type. `data-*` and `aria-*` stay open as an escape hatch. Two boundaries worth knowing: - **React 19 is required** for `on*` event props on custom elements and for non-string attribute values. On React 18 the declarations still type your markup; pass primitives as attributes and bind events with a ref. - **Angular** has no type-level checking for unknown elements beyond `CUSTOM_ELEMENTS_SCHEMA`, which disables checking rather than adding it. There is nothing to ship for Angular, so nothing is shipped. Timeless elements work there; their attributes are simply unchecked. Qwik is not covered. Its JSX module moved between major versions and its custom-event prop convention differs from the frameworks above, so no declaration is published rather than one that might be wrong. ## Authoring CSS-only components `ui-*` tags are tags, so every typing above and the editor data complete them precisely. CSS-only components are a root class plus `data-ui-*` on a native tag, which no editor can complete per element. `@timelessui/components/attributes` is the typed answer: `uiAttributes` returns the attribute object and `uiAttributeString` the serialized form, both keyed to each component's permitted values. Neither is reachable from the default import. See [Editor setup](/docs/getting-started/editor-setup/#the-one-gap-css-only-components) for why the gap exists, worked examples, and the development-time validator. ## Server rendering Every import above is side-effect free except the stylesheets and the `register/*` entrypoints. Class imports and `define/*` imports never call `customElements.define`, so they are safe in server code and test utilities — `define/*` hands you the function and leaves the calling to you. `register/*` is the one that registers on import, and it is inert on the server, where there is no `customElements` to define into. That is deliberate: a universal module can import it without crashing the render, and the client bundle re-imports the same module and registers for real. The consequence is worth stating plainly — a `register/*` import that only ever executes on the server registers nothing at all, so it belongs in code the browser loads. When you would rather be explicit about the timing, import `define/*` and call the function yourself. See [Core concepts](/docs/concepts/) for what registration does in a browser realm. --- # What Timeless does not ship Source: https://timeless.build/docs/reference/scope.md The components Timeless deliberately leaves out, the reasoning behind each one, and the one that is deferred rather than refused. Timeless is a library of primitives over the web platform. That is a boundary as much as a goal: some widely shipped components are outside it, and saying which ones is more useful than leaving the question to be re-argued per pull request. Each row below is a decision, not an omission. Two tests decide them. Does the platform already do this in a line or two of CSS? Then a component would only be a worse way to reach the same declaration. Does it need domain machinery — a locale database, a layout engine, a virtualiser? Then it is a library in its own right, and wrapping one is not a primitive. Everything here is buildable on top of Timeless. Nothing here is a gap someone needs to fill before the library is usable. ## The platform already does it | Not shipping | Because | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Aspect Ratio** | `aspect-ratio: 16 / 9` is one declaration. A component around it adds a class to learn and a stylesheet to load in exchange for nothing. | | **Scroll Area** | Custom scrollbars fight momentum scrolling, trackpad inertia, and platform conventions, and they routinely drop keyboard and assistive-technology access. `scrollbar-color`, `scrollbar-width`, and `scrollbar-gutter` are Baseline and keep the native scroller. | | **Carousel** | Almost every implementation wraps a third-party engine. CSS scroll snap with scroll-marker and scroll-button pseudo-elements is the platform answer, and it keeps native swipe, keyboard, and scroll-anchoring behavior. | ## It is a library, not a primitive | Not shipping | Because | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Chart** | Scales, axes, ticks, legends, and stacking are a charting library's whole surface area. Timeless would either wrap one or reimplement it badly. | | **Data Table** | Sorting, filtering, column resizing, pagination, and virtualisation belong to a table library. The CSS [Table](/docs/components/table/) stays: it styles the markup one produces. | | **Tree View** | The hardest pattern in the ARIA Authoring Practices Guide — typeahead, expand/collapse, multi-select across levels — with the least reuse across products. | The same test cuts both ways. OKLCH and OKLab parsing, gamut mapping, and contrast computation are a color library, so they ship as `@timelessui/color` rather than from the components package. [Color Picker](/docs/components/color-picker/) and [Color Swatch](/docs/components/color-swatch/) stay components: they are primitives that depend on that library, the same way every component depends on a platform capability. What decides the boundary is the domain machinery, not the subject matter. ## It is composition, not a component | Not shipping | Because | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sidebar and app shells** | A shell is a layout decision that belongs to the application. It is grid, [Separator](/docs/components/separator/), and whatever navigation the product needs. | | **Chat and AI surfaces** | Message lists, streaming indicators, and prompt composers are product surfaces. They are built out of primitives; they are not primitives. | | **Command palette** | [Combobox](/docs/components/combobox/) inside a [Dialog](/docs/components/dialog/), which is composition rather than a new contract. It is [documented as a recipe](/docs/components/select/) rather than shipped as a component. | | **Navigation menu** | Several triggers, one panel open at a time. [Hover Card](/docs/components/hover-card/) per trigger already gives the intent delay, the anchored panel, the `aria-expanded` wiring, and Escape — and a nav of links must not be an APG menu, which is the mistake a component here would make easy. [Documented as a recipe](/docs/components/menu/). | ## Deferred, not refused **Date Picker and Calendar.** `` covers the common case today with a native picker, native validation, and native form behavior, and it is the right default for most forms. A real calendar is a different project: month grids across locales, week-start rules, non-Gregorian calendars, ranges, and the APG grid pattern's full keyboard contract. That is worth doing properly or not at all, so it is on the list to revisit rather than ruled out. Until then, use `` with [Field](/docs/components/field/) for labelling, description, and error wiring. --- # Loading CSS Source: https://timeless.build/docs/styling/css.md The three CSS tiers, which two are required, and how to replace the theme with your own. import { Aside } from '@astrojs/starlight/components' Timeless CSS ships as plain stylesheets in three tiers. There is no build step, no preprocessor, and no configuration — but two of the three tiers are required, and it is worth knowing which. | Tier | Required | Holds | | ----------------------------------------- | -------- | ------------------------------------------------------------------------ | | `tokens.css` | Yes | The cascade-layer order, and `color-scheme` | | `core.css`, or `core/.css` | Yes | Behavior: anchoring, box participation, scrolling, native control resets | | `themes/atmosphere.css`, or per component | No | The look: colour, type, spacing, shadow, radius, motion | Core is the tier people are surprised by. It is not styling — it is the implementation of "the surface opens beside its trigger", "the closed menu is not on screen", and "the filtered option is gone". Without it a Select opens unanchored and a filtered option stays visible, which is a broken component rather than an unstyled one. ## One stylesheet, or only what you use For prototypes, documents, and anything where request granularity does not matter, import the theme — it pulls in the two required tiers itself: ```js import '@timelessui/components/css/themes/atmosphere.css' ``` For applications, import the required tiers plus the exact component a route renders: ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/button.css' import '@timelessui/components/css/core/dialog.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/button.css' import '@timelessui/components/css/themes/atmosphere/dialog.css' ``` Both paths produce the same public classes, custom properties, layers, and state selectors. The only difference is how much CSS reaches the browser. Each component page lists every stylesheet that component needs under **Styling**, core files included. There is no default theme. `themes/atmosphere.css` names the theme it loads, and a second theme would be a sibling of the same shape — `themes/.css` beside `themes//`. ## Always import tokens first `tokens.css` declares the order of the three Timeless layers: ```css @layer ui.tokens, ui.components, ui.utilities; ``` Layers are ordered by first encounter, so whichever stylesheet is parsed first gets the lower-priority layers. Load a component stylesheet before `tokens.css` and the order is established by that file instead, which is how a rule you wrote in `ui.utilities` ends up losing to a component. `themes/atmosphere.css` imports tokens first on your behalf. When importing granularly, put `tokens.css` first yourself. `tokens.css` also sets `color-scheme: light dark`, which is what makes `light-dark()` resolve to the branch the reader asked for. Drop it and every scheme-dependent token silently returns its light value. See [Theming](/docs/styling/theming/) for what those layers mean in practice and which tokens and component variables you can override. ## Bringing your own theme The theme tier is genuinely optional, and replacing it is the supported path — for a design system of your own, for Tailwind, or for any utility-class or CSS-in-JS setup. Import the two required tiers and nothing else: ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core.css' ``` Every component is then positioned, structurally intact, and operable, with no appearance of ours: UA colours, no radius, no shadow, the browser's default face. Style the public anatomy with whatever you like — [Theming](/docs/styling/theming/#styling-without-the-atmosphere-theme) covers the selectors that are yours to target. A colour, radius, shadow, or type utility can never lose to Timeless on this path, and not by convention: core is forbidden from declaring any of those properties, and a build check proves it. The only properties core does declare are the behavioural ones — `display`, `overflow`, `position`, the `inset` family, `appearance` and friends — so those are the only conflicts possible, and they are conflicts you would be picking deliberately. Two things to expect on this path. Sizing stays in the theme, so a few components sit at their content size rather than a designed one — a Sheet panel is content-height instead of full-height until you give it a size, and a Select surface is as wide as its widest option instead of as wide as its trigger, because `min-inline-size: anchor-size(width)` is a design decision. And core carries no affordances, so there is no hover highlight and no focus ring until you add them. Target size comes with the sizing, which makes it yours. Without the theme, seven components fall under the 24×24 CSS pixels [WCAG 2.2 SC 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) asks for — the colour picker's channel sliders, the number stepper's step buttons, pagination's page links, and the recipes built from them. A control can still conform by being spaced rather than large, which is why core does not force a floor on you: a dense tool that wants small targets is a legitimate design, and picking between the two is exactly the kind of decision the theme tier holds. Whichever you pick, decide it rather than inheriting it. `apps/e2e` records the seven and fails on an eighth, so this list stays honest. --- # Theming Source: https://timeless.build/docs/styling/theming.md Override Timeless styles with cascade layers, design tokens, and component variables. import { Aside } from '@astrojs/starlight/components' import TokenTable from '../../../../components/docs/TokenTable.astro' Timeless ships CSS in three tiers — the layer order, behavior, and the look — and only the last is optional. [Loading CSS](/docs/styling/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. ## Cascade layers come first `tokens.css` opens with the layer order, so every Timeless rule lands in a named layer: ```css @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`: ```css /* 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. ## Design tokens 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: ```css :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. ### Color ### Control fills ### Radius ### Shadow ### Space ### Typography ### Motion ### Effects ## Light and dark `tokens.css` sets `color-scheme: light dark` and expresses every scheme-dependent token with `light-dark()`: ```css --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: ```css /* 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: ```css :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: ```css --ui-bg-accent-hover: color-mix(in oklab, #0064d8 78%, #0045b7); ``` ## Component variables 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: ```css /* 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. ## Styling without the Atmosphere theme 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](/docs/styling/css/#bringing-your-own-theme) 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](/docs/styling/utility-css/) works one through end to end: ```css [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. --- # Utility CSS and Tailwind Source: https://timeless.build/docs/styling/utility-css.md Style Timeless with Tailwind or any utility framework — the import order that decides who wins, a complete worked example, and the four conflicts that are possible. import { Aside } from '@astrojs/starlight/components' Utility CSS is a supported path, not a workaround. Timeless ships behavior and appearance in separate tiers precisely so the appearance can be yours: import `tokens.css` and the core layer, skip the theme, and every component is positioned, operable, and structurally intact with no look of ours to fight. From there a `rounded-lg` or a `bg-white` is not overriding anything — there is nothing to override. One thing decides whether that works, and it fails silently when it is wrong: the import order. ## Import Timeless before Tailwind Tailwind v4 emits native cascade layers, and CSS orders layers by first encounter. So whichever stylesheet is parsed first gets the lower-priority layers, and layer order beats specificity outright. ```css /* Timeless first: `ui.*` registers first, so every Tailwind layer lands above it. */ @import '@timelessui/components/css/tokens.css'; @import '@timelessui/components/css/core.css'; @import 'tailwindcss'; ``` That produces this layer order, which you can read in the compiled output: ```css @layer ui.tokens, ui.components, ui.utilities; @layer theme, base, components, utilities; ``` Invert the two imports and `ui.components` lands _after_ `utilities`. A utility for a property core declares then loses, with no error and nothing in the console: | Import order | `overflow-visible` on a Popover surface | Result | | ----------------------- | --------------------------------------- | -------------------------- | | Timeless, then Tailwind | wins | `overflow: visible` | | Tailwind, then Timeless | loses to `core/popover.css` | `overflow: auto`, no error | Only the properties core declares can conflict at all — `display`, `overflow`, `position`, the `inset` family, `appearance`, and their neighbours. Core is forbidden from declaring a colour, radius, shadow, type, or size property, and a build check proves it, so a `bg-*`, `rounded-*`, `shadow-*`, or `text-*` utility cannot lose to Timeless whatever the order. Tailwind v3 emits unlayered rules, which beat every layered rule regardless of order, so v3 wins either way. ## A Popover, styled entirely in Tailwind This is the whole thing: no theme CSS, one core stylesheet per component, and every visual decision in a utility class. ```css /* app.css */ @import '@timelessui/components/css/tokens.css'; @import '@timelessui/components/css/core/floating.css'; @import '@timelessui/components/css/core/popover.css'; @import 'tailwindcss'; ``` ```js import { definePopoverElement } from '@timelessui/components/define/ui-popover' definePopoverElement() ``` ```html

Panel

Anchored by core CSS, styled by Tailwind utilities.

``` What each side contributes is worth being explicit about, because it is the same division for every component: - **The platform** opens and closes the surface, light-dismisses it, handles Escape, and puts it in the top layer. `popovertarget` and `popover` do that before any script runs. - **Core CSS** gives the host `display: contents`, makes the panel scroll, and anchors it under the trigger with `position-area`. - **Registration** adds what neither of those does: `aria-controls`, `aria-expanded`, `aria-haspopup`, and `role="dialog"` on the surface. - **Your utilities** supply every pixel of the look, including the gap between trigger and surface — that is the `m-2`, which replaces the margin the theme would have set. ## Four conflicts, and what to do about them Everything above is the happy path. These are the four places where utility CSS and the platform interact in a way worth knowing in advance. ### The host already has a display; do not give it one An unknown element defaults to `display: inline`, and Tailwind's Preflight does not reset custom elements. Core does it for you — `ui-popover` is `display: contents`, `ui-combobox` is `display: grid`, and so on — so there is nothing to hand-write. The corollary follows from the import order above: because utilities win, a `block` or `flex` utility _on the host_ defeats `display: contents` and puts a box between the trigger and its surface. Style the parts, not the host. ### Style the surface's colour, not only its background With no theme loaded, a `[popover]` or `` takes the UA's `color: CanvasText` and `background-color: Canvas`. `tokens.css` keeps `color-scheme: light dark` on purpose, so those system colours follow the reader's preference — which means on a dark-preferring browser `CanvasText` is white. A `bg-white` utility with no `text-*` alongside it gives you white text on a white panel, in one theme only, which is a hard bug to catch. Set both, as the example above does: ```html
``` ### Filtered options and `[hidden]` Option filtering, paging, empty groups, and the pager all express themselves by setting the native `hidden` attribute, and core hides them with `display: none`. A `display` utility on an option row is therefore a rule about the same property as the rule that hides it. Tailwind's Preflight already resolves this in your favour. It ships ```css [hidden]:where(:not([hidden='until-found'])) { display: none !important; } ``` so with the default `@import "tailwindcss"` a filtered option stays hidden even under `flex`. If you import Tailwind without Preflight, that rule is gone, and a `flex` on an option row makes every filtered option reappear. Guard the variant instead of the property: ```html
``` ### `:state()` has no built-in variant A few components expose state through `ElementInternals` rather than an attribute — `ui-toast` has `--closed`, `ui-color-picker` has `--copied` and `--contextual`, `ui-sheet` has `--dragging`. Tailwind has no variant for `:state()`, but the arbitrary form compiles and matches: ```html ``` Everything else Timeless exposes is a native attribute, an ARIA attribute, or a platform pseudo-class, all of which Tailwind already has variants for: `aria-expanded:*`, `aria-selected:*`, `disabled:*`, `data-[ui-variant=primary]:*`, and arbitrary selectors such as `[&[data-ui-part~='trigger']]:*` for authored anatomy. `open:*` is worth knowing on this library in particular — in v4 it compiles to `:is([open], :popover-open, :open)`, so one variant covers an open `` and an open popover surface alike. ## What to target The public anatomy is the same with or without a theme, so it is what your utilities and your own CSS attach to: `.ui-*` roots, `data-ui-*` configuration on those roots, plain attributes on `ui-*` hosts, `data-ui-part` for authored anatomy, and native attributes plus ARIA for state. [Theming](/docs/styling/theming/#styling-without-the-atmosphere-theme) lists them with examples. Two more things to expect on the theme-free path, neither specific to Tailwind: sizing lives in the theme, so a few components sit at their content size until you give them one, and core carries no affordances, so there is no hover highlight and no focus ring until you add them. [Loading CSS](/docs/styling/css/#bringing-your-own-theme) covers both. --- # Alert Communicate status without replacing native live-region semantics. (CSS only.) Reference: https://timeless.build/docs/components/alert/ ## Markup ```html

Package published

The component is available to downstream apps.

``` ## Install ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/alert.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/alert.css' import '@timelessui/components/css/themes/atmosphere/link.css' ``` ## Anatomy Parts are authored in your own markup and identified by the selector below. Required parts must be present for the component to work. Private `data-ui-internal-*` hooks are written by the runtime and must never be authored. | Part | Required | Selector | Purpose | | --- | --- | --- | --- | | `icon` | No | `[data-ui-part~='icon']` | Decorative status icon. Mark it `aria-hidden="true"`. | | `content` | No | `[data-ui-part~='content']` | Wrapper for the title and description. | | `title` | No | `[data-ui-part~='title']` | Short summary line. | | `description` | No | `[data-ui-part~='description']` | Supporting detail. | | `actions` | No | `[data-ui-part~='actions']` | Container for one or two follow-up actions. | ## Attributes Every value below is implemented by the stylesheets this component ships. Boolean attributes are presence-based: author the attribute with no value, or omit it. | Attribute | Values | Default | Description | | --- | --- | --- | --- | | `data-ui-variant` | `neutral` · `accent` · `success` · `warning` · `danger` | `neutral` | Status intent. This is styling only — set `role="status"` or `role="alert"` yourself to control how assistive technology announces the message. | | `data-ui-density` | `compact` · `normal` | `normal` | Internal spacing. | ## Styling Root identity: `ui-alert`. Required stylesheets: `tokens.css`, `core/alert.css`, `themes/atmosphere/tokens.css`, `themes/atmosphere/alert.css`, `themes/atmosphere/link.css`. This component adds no custom properties of its own. Restyle it through the design tokens at https://timeless.build/docs/styling/theming/ or your own CSS. Design tokens this component reads (23), global and set at the theme level: `--ui-accent`, `--ui-accent-hover`, `--ui-accent-soft`, `--ui-bg-surface`, `--ui-danger`, `--ui-danger-soft`, `--ui-duration-fast`, `--ui-ease-standard`, `--ui-fg`, `--ui-fg-muted`, `--ui-focus`, `--ui-line`, `--ui-radius-lg`, `--ui-radius-xs`, `--ui-space-1`, `--ui-space-2`, `--ui-space-3`, `--ui-space-4`, `--ui-space-5`, `--ui-success`, `--ui-success-soft`, `--ui-warning`, `--ui-warning-soft`. ## Accessibility Keep the native elements, roles, and relationships shown in the markup. Timeless adds only the state and keyboard coordination the platform does not already provide, and it never supplies your accessible names — those depend on your content. Follows the [Alert pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) from the ARIA Authoring Practices Guide. The role is yours to choose, and it depends on when the alert appears. Something rendered with the page is not an alert at all — give it `role="status"`, or no role and a heading, because a live region announces *changes* and there is no change on first paint. Something inserted in response to what the user just did is a live region: `role="status"` to wait its turn, `role="alert"` to interrupt, and the latter only when the message cannot wait. The `icon` part is decorative and must be hidden from assistive technology; `data-ui-variant` carries no meaning on its own, so say in the text what the colour implies. ## Before JavaScript runs This primitive is CSS only. There is nothing to register and nothing to wait for. --- # Avatar Identity fallback and presence indicators. (CSS only.) Reference: https://timeless.build/docs/components/avatar/ ## Markup ```html AS ``` ## Install ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/avatar.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/avatar.css' ``` ## Anatomy Parts are authored in your own markup and identified by the selector below. Required parts must be present for the component to work. Private `data-ui-internal-*` hooks are written by the runtime and must never be authored. | Part | Required | Selector | Purpose | | --- | --- | --- | --- | | `image` | No | `[data-ui-part~='image']` | The ``. Give it an empty `alt` when a label follows. | | `fallback` | No | `[data-ui-part~='fallback']` | Initials or icon shown when no image loads. | | `status` | No | `[data-ui-part~='status']` | Presence dot. Decorative; keep it `aria-hidden="true"`. | ## Attributes Every value below is implemented by the stylesheets this component ships. Boolean attributes are presence-based: author the attribute with no value, or omit it. | Attribute | Values | Default | Description | | --- | --- | --- | --- | | `data-ui-size` | `sm` · `md` · `lg` | `md` | Avatar diameter. | | `data-ui-shape` | `circle` · `rounded` · `square` | `circle` | Corner treatment. | | `data-ui-status` | `online` · `away` · `busy` · `offline` | — | Presence indicator color. Omit the attribute to hide the indicator. The dot is decorative, so also expose the status in text. | ## Styling Root identity: `ui-avatar`. Required stylesheets: `tokens.css`, `core/avatar.css`, `themes/atmosphere/tokens.css`, `themes/atmosphere/avatar.css`. This component adds no custom properties of its own. Restyle it through the design tokens at https://timeless.build/docs/styling/theming/ or your own CSS. Design tokens this component reads (11), global and set at the theme level: `--ui-bg-page`, `--ui-bg-surface-raised`, `--ui-danger`, `--ui-fg`, `--ui-fg-subtle`, `--ui-line`, `--ui-radius-lg`, `--ui-radius-pill`, `--ui-radius-sm`, `--ui-success`, `--ui-warning`. ## Accessibility Keep the native elements, roles, and relationships shown in the markup. Timeless adds only the state and keyboard coordination the platform does not already provide, and it never supplies your accessible names — those depend on your content. This component declares no keyboard contract, because it has none of its own: the markup above is native elements and native ARIA, and everything a keyboard or a screen reader does with it comes from the platform. Nothing here manages focus, and nothing here adds a role you did not author. So the accessibility of this component is the accessibility of the markup — which is why the markup is the part worth copying exactly. ## Before JavaScript runs This primitive is CSS only. There is nothing to register and nothing to wait for. --- # Badge Compact status and metadata labels. (CSS only.) Reference: https://timeless.build/docs/components/badge/ ## Markup ```html Stable ``` ## Install ```js import '@timelessui/components/css/tokens.css' import '@timelessui/components/css/core/badge.css' import '@timelessui/components/css/themes/atmosphere/tokens.css' import '@timelessui/components/css/themes/atmosphere/badge.css' ``` ## Anatomy Parts are authored in your own markup and identified by the selector below. Required parts must be present for the component to work. Private `data-ui-internal-*` hooks are written by the runtime and must never be authored. | Part | Required | Selector | Purpose | | --- | --- | --- | --- | | `dot` | No | `[data-ui-part~='dot']` | Leading status dot. Decorative. | ## Attributes Every value below is implemented by the stylesheets this component ships. Boolean attributes are presence-based: author the attribute with no value, or omit it. | Attribute | Values | Default | Description | | --- | --- | --- | --- | | `data-ui-variant` | `neutral` · `accent` · `success` · `warning` · `danger` · `outline` | `neutral` | Status intent. | | `data-ui-size` | `sm` · `md` · `lg` | `md` | Badge height and font size. | ## Styling Root identity: `ui-badge`. Required stylesheets: `tokens.css`, `core/badge.css`, `themes/atmosphere/tokens.css`, `themes/atmosphere/badge.css`. This component adds no custom properties of its own. Restyle it through the design tokens at https://timeless.build/docs/styling/theming/ or your own CSS. Design tokens this component reads (14), global and set at the theme level: `--ui-accent-soft`, `--ui-bg-control-muted`, `--ui-danger`, `--ui-danger-soft`, `--ui-fg`, `--ui-line`, `--ui-radius-pill`, `--ui-space-1`, `--ui-space-2`, `--ui-space-3`, `--ui-success`, `--ui-success-soft`, `--ui-warning`, `--ui-warning-soft`. ## Accessibility Keep the native elements, roles, and relationships shown in the markup. Timeless adds only the state and keyboard coordination the platform does not already provide, and it never supplies your accessible names — those depend on your content. This component declares no keyboard contract, because it has none of its own: the markup above is native elements and native ARIA, and everything a keyboard or a screen reader does with it comes from the platform. Nothing here manages focus, and nothing here adds a role you did not author. So the accessibility of this component is the accessibility of the markup — which is why the markup is the part worth copying exactly. ## Before JavaScript runs This primitive is CSS only. There is nothing to register and nothing to wait for. --- # Breadcrumb A trail of links to the pages above this one. CSS only. (CSS only.) Reference: https://timeless.build/docs/components/breadcrumb/ > **Choosing between components.** A breadcrumb says where the current page sits in a hierarchy. For moving between siblings at the same level use [Tabs](/docs/components/tabs/), and for a list of pages in a sequence use [Pagination](/docs/components/pagination/). > **Markup you author.** Give the `