Skip to main content

Getting started

Install

pnpm add @zuilib/components react react-dom @headlessui/react
PeerVersion
react, react-dom^18.0.0
@headlessui/react^2.2.0
tailwindcss^4.0.0, optional: only for the two Tailwind entries below

@zuilib/tokens is a dependency of the package and is loaded by every stylesheet entry; you do not install it separately.

Load one stylesheet

The components are written in Tailwind v4 utility classes that map to the tokens. Load exactly one of these four entries. Each gives you the tokens, the base styles and every utility the components use; they differ in who runs Tailwind and whether Tailwind's preflight reset is included.

Your appImportWhat it is
Tailwind v4, and you do not @import "tailwindcss" yourself@zuilib/components/styles.cssAll-in-one Tailwind source: @import "tailwindcss" (with preflight) + the component @source + tokens. Your pipeline compiles it
Tailwind v4, and you already @import "tailwindcss"@zuilib/components/tailwind.cssTailwind source without Tailwind: the component @source + tokens + the slider's vendor CSS. Import it after your tailwindcss import
No Tailwind@zuilib/components/zui.cssPrebuilt, flat CSS with Tailwind's preflight reset. Nothing to process
No Tailwind, own reset / base styles@zuilib/components/zui-no-preflight.cssThe same prebuilt CSS from Tailwind's theme and utilities layers only: no preflight, so your reset stays in charge

Importing styles.css twice or next to a second @import "tailwindcss" duplicates Tailwind; use tailwind.css when Tailwind is already there.

Tailwind app, one line

styles.css is a Tailwind source file. Import it from your app's CSS so your Tailwind pipeline (@tailwindcss/vite, @tailwindcss/postcss, the CLI) processes it. That also lets you use the token utilities (bg-primary, rounded-md, text-muted-foreground, …) in your own markup.

/* app.css */
@import "@zuilib/components/styles.css";

Tailwind app that already imports Tailwind

For apps with their own source(), @theme or plugin setup:

/* app.css */
@import "tailwindcss";
@import "@zuilib/components/tailwind.css";

tailwind.css carries @source "../**/*.{js,ts,tsx}", resolved relative to the file, so the component JS under node_modules/@zuilib/components/dist is scanned even though Tailwind skips node_modules by default.

App without Tailwind

zui.css is the prebuilt, flat bundle; zui-no-preflight.css the same without the reset. Import one at the app entry:

// main.tsx
import '@zuilib/components/zui.css' // or '@zuilib/components/zui-no-preflight.css'

Importing styles.css or tailwind.css from JavaScript or a non-Tailwind bundler does not work: their @import "tailwindcss" and @source are only meaningful to Tailwind.

Import components

One subpath per component; the default export is the component. There is no barrel, so a bundle carries only what it imports.

import Button from '@zuilib/components/button'
import Input from '@zuilib/components/input'
import Listbox from '@zuilib/components/listbox'

<Listbox value={value} onChange={setValue}>
<Listbox.Button>{value?.label ?? 'Pick one'}</Listbox.Button>
<Listbox.Options>
{options.map((o) => <Listbox.Option key={o.id} value={o}>{o.label}</Listbox.Option>)}
</Listbox.Options>
</Listbox>

Compound parts are attached as statics (Listbox.Option) and also exported by name (ListboxOption). Shared helpers live under lib/:

SubpathExports
@zuilib/components/lib/cncn(): clsx + tailwind-merge, the way the components merge className
@zuilib/components/lib/iconsThe glyphs the components share (ChevronDownIcon, CheckIcon, XIcon, SearchIcon, status icons)
@zuilib/components/lib/spinnerSpinner
@zuilib/components/lib/focusThe focus-ring class strings
@zuilib/components/lib/sizesControlSize and the shared size maps
@zuilib/components/lib/form-item-contextuseFormItem() for custom controls inside a FormItem
@zuilib/components/lib/transitionsThe data-closed enter / leave recipes for Headless UI's transition

Dark mode

The tokens switch on a dark class. Add it to <html> for the whole app or to any wrapper for a dark region; the components read the variables and need nothing else. color-scheme is set with it, so native controls and scrollbars follow.

document.documentElement.classList.toggle('dark', prefersDark)
<section class="dark">…a dark panel on a light page…</section>

React Server Components

Every component file begins with 'use client', which the build keeps in the published JS. Import a component from a server component as usual; it becomes a client boundary on its own. Nothing in the package reads the DOM at import time, so server rendering is safe; overlays (Dialog, Drawer, Menu, Popover) portal to <body> on the client.

A first form

import '@zuilib/components/zui.css'
import Button from '@zuilib/components/button'
import Description from '@zuilib/components/description'
import FormItem from '@zuilib/components/form-item'
import Input from '@zuilib/components/input'
import Label from '@zuilib/components/label'
import Message from '@zuilib/components/message'

export function SignIn() {
return (
<form className="flex max-w-sm flex-col gap-4">
<FormItem>
<Label required>Email</Label>
<Input type="email" required fullWidth />
<Description>Work address, please.</Description>
</FormItem>
<FormItem invalid>
<Label>Password</Label>
<Input type="password" fullWidth />
<Message>At least 12 characters.</Message>
</FormItem>
<Button type="submit">Sign in</Button>
</form>
)
}

FormItem gives the control its id, aria-labelledby, aria-describedby, aria-invalid and disabled state. See FormItem.

Next

  • Theming: make it yours with a few CSS variables.
  • Registry: copy single components in as source with the zui CLI.
  • Tokens: the full variable contract.