Skip to content

React

Terminal window
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:

import '@timelessui/components/react'

The declaration is types-only. Timeless does not add a runtime wrapper or React dependency.

Import CSS and registration from a client entrypoint:

import '@timelessui/components/css/tokens.css'
import '@timelessui/components/css/listbox.css'
import '@timelessui/components/define/ui-listbox'

Load definition imports from a client entrypoint. CSS and class-only imports are safe during server rendering.

Use the element directly:

export function StatusPicker() {
return (
<ui-listbox value="draft" role="listbox" aria-label="Status">
<div role="option" data-ui-value="draft">
Draft
</div>
<div role="option" data-ui-value="ready">
Ready
</div>
</ui-listbox>
)
}

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.

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<TabsChangeDetail> 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.

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:

import { uiAttributes } from '@timelessui/components/attributes'
export function PublishButton() {
return (
<button {...uiAttributes('button', { variant: 'primary', size: 'lg' })} type="button">
Publish
</button>
)
}

variant accepts only the seven button variants, and Card’s filled is a type error on a Button. See Editor setup for what each editor completes.