---
title: Vanilla
description: Use Timeless directly from HTML and JavaScript.
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

## 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
<ui-toggle-group selection="single" aria-label="Alignment">
  <button class="ui-button ui-toggle" value="left" aria-pressed="true">Left</button>
  <button class="ui-button ui-toggle" value="center" aria-pressed="false">Center</button>
</ui-toggle-group>
```

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 = `<button ${uiAttributeString('button', { variant: 'primary' })} type="button">Publish</button>`

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.