# ZUI — full reference > Generated from packages/components/README.md and packages/tokens/src. Site map: https://zuilib.com/llms.txt. Registry: https://zuilib.com/r/latest/index.json. ## Install ```bash pnpm add @zuilib/components @zuilib/tokens react react-dom @headlessui/react # or, as source: npx @zuilib/cli init && npx @zuilib/cli add button ``` ## Setup (required once per app) Load exactly one of the four stylesheets. 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 app | Import | What it is | |---|---|---| | Tailwind v4, and you do **not** `@import "tailwindcss"` yourself | `@zuilib/components/styles.css` | All-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.css` | Tailwind source without Tailwind: the component `@source` + tokens (`@zuilib/tokens/tailwind.css` + `tokens.css`) + the slider's vendor CSS. Import it after your `tailwindcss` import | | No Tailwind | `@zuilib/components/zui.css` | Prebuilt, flat CSS with Tailwind's preflight reset. Nothing to process | | No Tailwind, own reset / base styles | `@zuilib/components/zui-no-preflight.css` | The same prebuilt CSS built 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; Tailwind's automatic source detection stays on, so the token utilities (`bg-primary`, `rounded-md`, …) work in your own markup with no extra `@source` line: ```css /* app.css */ @import "@zuilib/components/styles.css"; ``` **Tailwind app that already imports Tailwind** (for its own `source()` / `@theme` / plugin setup): ```css /* 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. `tailwindcss` is an optional peer dependency for both Tailwind paths (`pnpm add -D tailwindcss @tailwindcss/vite`). **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: ```tsx // 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"` / `@source` are only meaningful to Tailwind. **Dark mode:** add or remove the `dark` class on `` or a root wrapper. ## Import rules | Rule | Detail | |------|--------| | **Default exports only** | `import Button from '@zuilib/components/button'` — no barrel `import { Button } from '@zuilib/components'` | | **One subpath per component** | Each file under `exports` in `package.json` is a separate import | | **Styles are separate** | Components do not auto-inject CSS; you must load one stylesheet: `styles.css` / `tailwind.css` (Tailwind app) or `zui.css` / `zui-no-preflight.css` (prebuilt). See Setup | | **Utilities** | `import { cn } from '@zuilib/components/lib/cn'` for class merging; `import Spinner from '@zuilib/components/lib/spinner'`; `lib/focus`, `lib/sizes`, `lib/form-item-context` for custom controls that should match | ## Exports | Subpath | Default export | Purpose | |---------|----------------|---------| | `@zuilib/components/styles.css` | — | Tailwind v4 source: Tailwind + tokens + `@source` for the components. Needs a Tailwind pipeline | | `@zuilib/components/tailwind.css` | — | Tailwind v4 source without `@import "tailwindcss"`: tokens + `@source` for the components, for apps that import Tailwind themselves | | `@zuilib/components/zui.css` | — | Prebuilt CSS: tokens + the utilities the components use + preflight. No Tailwind needed | | `@zuilib/components/zui-no-preflight.css` | — | Prebuilt CSS without Tailwind's preflight reset, for apps with their own reset | | `@zuilib/components/button` | `Button` | Primary actions | | `@zuilib/components/ai-button` | `AIButton` | Button with sparkle / generating state | | `@zuilib/components/input` | `Input` | Text input | | `@zuilib/components/textarea` | `Textarea` | Multiline text | | `@zuilib/components/rich-textarea` | `RichTextArea` | Textarea with label, description, error, char count | | `@zuilib/components/checkbox` | `Checkbox` | Boolean / indeterminate toggle | | `@zuilib/components/switch` | `Switch` | Boolean toggle | | `@zuilib/components/select` | `Select` | Native ` ) } ``` ## Component API ### Shared conventions Every component follows the same contract, so what you learn on one applies to the rest: | Convention | Detail | |---|---| | `size` | `'sm' \| 'md' \| 'lg'` on every sized control (Button adds `'icon'`, Spinner and Avatar add `'xl'`, Table and Badge stop at `'md'`). Heights and paddings come from `--control-height-*` / `--control-padding-x-*`; one density retheme moves every component | | `invalid` | Sets `aria-invalid` + `data-invalid` and the destructive border / ring. Leave it unset inside a `FormItem invalid` to inherit it | | `disabled` | Leave it unset inside a `FormItem disabled` / `Fieldset disabled` to inherit it (Headless UI only inherits when the prop is `undefined`) | | `fullWidth` | `w-full` on the root. Defaults to `false` on Input / Textarea / Select / Button and to `true` on Listbox / Combobox | | `name` / `form` | Native form participation. Listbox, Combobox, RadioGroup, Checkbox and Switch render hidden inputs for it | | `anchor` / `portal` | Listbox and Combobox: where the panel floats (`'bottom start'` default, `false` for an inline panel) and whether it is portalled to `` (default `true` when anchored). Headless UI portals every anchored panel regardless of `portal`, so to keep a panel inside a themed subtree use `anchor={false}` | | `className` | Merged last onto the root. Controls that wrap a native element in a frame also take `inputClassName` / `textareaClassName` for the element itself | | Native reset | Every part that renders a native ` ``` | Prop | Type | Default | |------|------|---------| | `variant` | `'primary' \| 'secondary' \| 'destructive' \| 'outline' \| 'ghost' \| 'link'` | `'primary'` | | `size` | `'sm' \| 'md' \| 'lg' \| 'icon'` | `'md'` — `icon` is a square the height of an `md` control | | `fullWidth` | `boolean` | `false` | | `loading` | `boolean` | `false` — shows a spinner, sets `aria-busy`, swallows clicks, keeps focus | | `loadingText` | `string` | `'Loading'` — visually hidden, announced with the spinner | | `leadingIcon` / `trailingIcon` | `ReactNode` | — `leadingIcon` is replaced by the spinner while `loading` | | `as` | `ElementType` | `'button'` — polymorphic: `