# chunks-ui — Full Documentation > React 19+ component library built on Base UI, Tailwind CSS v4, and Motion. --- # Getting Started ## Installation ```bash bun add chunks-ui motion ``` Peer dependencies: | Package | Range | Notes | | --------------------------- | --------- | ------------------------------------------------ | | `react`, `react-dom` | `^19.2.0` | required; React 19.0 and 19.1 are untested | | `@base-ui/react` | `^1.9.0` | required | | `class-variance-authority` | `^0.7.1` | required | | `tailwindcss` | `^4.0.0` | optional peer; you need Tailwind v4 in the app to use `theme.css` | | `motion` | `^12.8.0` | optional; without it components use CSS transitions | Node 22 or newer. The install command does not add `tailwindcss`: the app is expected to have Tailwind v4 set up already. The examples use icons from `lucide-react`, which you install yourself (`bun add lucide-react`); any icon library works. Installing `motion` in any dependency field turns the animations on. The types do not need Motion. For Motion's full types, cast in your own code: `useMotion() as typeof import("motion/react") | null`. `useReducedMotion()` returns `true` when the user has asked for reduced motion (`prefers-reduced-motion: reduce`) and `false` otherwise. It follows the setting if the user changes it while the page is open. Use it to skip an animation that you start from JavaScript, or to show a static fallback: ```tsx import { Button, useReducedMotion } from "chunks-ui"; export function BackToTop() { const reduced = useReducedMotion(); return ( ); } ``` On the server and while hydrating, the hook returns `false`, so the client markup matches what the server sent. For a user who asks for reduced motion, it switches to `true` right after hydration. A component that mounts after hydration, such as a popup that opens later, reads the real value on its first render. An animation that must be off from the first paint belongs in CSS (the `motion-reduce:` variant), not in the hook. ## Setup Add the theme to the CSS file that imports Tailwind (e.g. `app/globals.css`): ```css @import "tailwindcss"; @import "chunks-ui/theme.css"; ``` Then import components as needed: ```tsx import { Button, Input, Tabs } from "chunks-ui"; ``` The theme CSS exposes OKLCH color tokens as CSS variables and registers them with Tailwind CSS v4 through `@theme`. Its `@source` tells Tailwind to scan the package for the classes the components use -- no plugin needed. Importing the theme sets the page's default font to Manrope through Tailwind's preflight (`--font-sans`), or to the browser's generic `sans-serif` when your app does not load Manrope (the font files are not included). To keep your app's font, set the token after the import: ```css @import "tailwindcss"; @import "chunks-ui/theme.css"; @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; } ``` Dark mode is the `.dark` class on ``: with it, every component uses the dark values of the tokens. See [Theme › Dark Mode](/theme#dark-mode). A React Server Component imports `cn`, `cnState` and the `*Variants` functions from `chunks-ui/utils`, which has no `"use client"`. See [Utilities › Server components](/utilities#server-components). ## Architecture - **Headless layer:** `@base-ui/react` v1.9+ (includes Floating UI for positioning) - **Styling:** Tailwind CSS v4 + CVA + `clsx` + `tailwind-merge` - **Animation:** Motion 12.8+ (optional peer dependency) - **Build:** tsup (ESM + CJS + `.d.ts`) - **Compound pattern:** Components use ``, ``, `` patterns - **Composition:** Base UI `render` prop, not `asChild` --- # Theme Chunks UI uses a CSS variable-based design token system following the [shadcn/ui](https://ui.shadcn.com) naming convention. Colors are defined in the OKLCH color space for perceptual uniformity (`--ring` is an OKLab mix of two of them). ## Setup Import the theme in the CSS file that imports Tailwind: ```css @import "tailwindcss"; @import "chunks-ui/theme.css"; ``` This single file provides all CSS variables for light mode, dark mode, and Tailwind v4 `@theme inline` tokens. It also includes an `@source` for the package, so Tailwind generates the classes the components use. ## Color System ### Semantic Colors (OKLCH) These values are the same in light and dark mode. Each foreground has at least 4.5:1 contrast (WCAG 2 AA) with its fill. | Token | Value | Purpose | |---|---|---| | `--primary` | `oklch(0.56 0.2165 257.21)` | Brand blue | | `--primary-foreground` | `oklch(0.985 0 0)` | Text on primary | | `--success` | `oklch(75.14% 0.1514 166.5)` | Positive actions | | `--success-foreground` | `oklch(0.145 0 0)` | Text on success | | `--warning` | `oklch(77.97% 0.1665 72.45)` | Caution states | | `--warning-foreground` | `oklch(0.145 0 0)` | Text on warning | | `--destructive` | `oklch(0.582 0.2249 25.88)` | Errors, deletions | | `--destructive-foreground` | `oklch(0.985 0 0)` | Text on destructive | ### Coloured Text Text in a semantic colour needs 4.5:1 contrast with the background. Some fills do not have it: success text on white is about 2:1. So coloured text uses its own colours: `primary-text`, `success-text`, `warning-text` and `destructive-text`, for example `text-destructive-text`. The `outlined` and `text` variants of `Button` and `IconButton`, `Chip`, `Field.Error`, the current day in `Calendar` and the hover colour of `ClearButton` use them. Each colour keeps the hue and chroma of its fill and changes only the lightness, with CSS relative colour syntax: | Mode | Lightness | Example | |---|---|---| | Light | `min(l, 0.51)`: at most 0.51 | `oklch(from var(--primary) min(l, 0.51) c h)` | | Dark | `max(l, 0.63)`: at least 0.63 | `oklch(from var(--primary) max(l, 0.63) c h)` | With these limits the default colours keep 4.5:1 on the background and on the `/10` tint that `Chip` uses. The `--colored-text-lightness` variable holds the rule for each mode. Only `.dark` redefines `--colored-text-lightness`. A light-styled subtree inside `.dark` should set `--colored-text-lightness: min(l, 0.51)` itself, and a dark surface without `.dark` should set `max(l, 0.63)`. The limits are tuned for the default fills. Very saturated teal or cyan custom fills (hue about 185 to 190, chroma 0.15 or more) can land below 4.5:1, so set `--primary-text` (or the matching `--X-text`) for them. The colour is computed on the element that uses it. If you set `--primary` on `:root` or on a nested element, the text shade inside follows it. To use a different shade, set `--primary-text`, `--success-text`, `--warning-text` or `--destructive-text`. These tokens have no default value. ```css .brand-section { --primary: oklch(0.55 0.2 293); /* fills, and the text shade derived from them */ --primary-text: oklch(0.45 0.2 293); /* optional: a shade of your choice */ --ring: color-mix(in oklab, var(--primary) 60%, var(--background)); /* the ring does not follow on its own */ } ``` The border of a control needs 3:1 contrast, and the success and warning fills have about 2:1 on white. So the `outlined` success and warning variants of `Button` and `IconButton` use `border-success-border` and `border-warning-border`. In light mode these cap the fill's lightness at 0.64 (success) and 0.67 (warning), which gives 3:1 on white, and a darker fill keeps its lightness. In dark mode they are the fill. `--success-border-lightness` and `--warning-border-lightness` hold the rule for each mode; a light subtree inside `.dark` sets them to `min(l, 0.64)` and `min(l, 0.67)` itself. To use a different shade, set `--success-border` or `--warning-border`. Relative colour syntax needs Chrome 119+, Safari 18+ or Firefox 128+. Older browsers show this text, and these borders, in the inherited text colour. ### Surface Colors | Token | Light | Dark | |---|---|---| | `--background` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | | `--foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | | `--card` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | | `--card-foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | | `--popover` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | | `--popover-foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | ### Neutral Palette | Token | Light | Dark | |---|---|---| | `--secondary` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | | `--secondary-foreground` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | | `--muted` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | | `--muted-foreground` | `oklch(0.53 0 0)` | `oklch(0.708 0 0)` | | `--accent` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | | `--accent-foreground` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | ### Utility Tokens | Token | Light | Dark | |---|---|---| | `--border` | `oklch(0.922 0 0)` | `oklch(0.269 0 0)` | | `--input` | `oklch(0.922 0 0)` | `oklch(0.269 0 0)` | | `--ring` | `color-mix(in oklab, var(--primary) 60%, var(--background))` | same as light | | `--radius` | `0.625rem` | `0.625rem` | | [`--touch-target-size`](/utilities#area-size) (optional) | `44px` (fallback, not defined) | `44px` (fallback, not defined) | ### Focus Ring Controls draw their keyboard focus ring in `--ring`, and so do the Popover, Menu and Dialog popups when they take focus themselves. By default it is 60% `--primary` and 40% `--background`, a softer shade of the primary: lighter in light mode, darker in dark mode. The mix is in OKLab, which has no hue to interpolate, so the ring keeps the primary's hue. Set `--ring` to use a colour of your choice. The ring is 2px wide and sits 2px outside the edge of the part, so it reaches 4px past it. `Tabs.Content`, whose container clips at the edge, draws it inside instead; so does `Drawer.Popup`, so that the border on its open side stays in view. To draw a part's ring flush with its edge, pass `focus-visible:outline-offset-0` in its `className`; on `Combobox.Control`, `NumberField.Group` and `Slider.Thumb`, which draw the ring while the control inside has focus, it is `has-[:focus-visible]:outline-offset-0`. On an `Input` with adornments, pass a function `className`: a string goes to the wrapper, not the input. `Radio.Item` and the `Calendar` buttons take no `className` for the control; to build a flush-ring radio, make the item from `Radio.Root` and `Radio.Indicator`. A custom property resolves its `var()` on the element that declares it, and its children inherit the result. `theme.css` declares `--ring` on `:root` and `.dark`, so a `--primary` or `--background` you set on `:root` changes the ring too. A nested element that sets `--primary` or `--background` keeps the ring of its parent: declare `--ring` on that element as well, as in the `.brand-section` example above. ## Tailwind Usage Variables are mapped to Tailwind utilities via `@theme inline`: ```css @theme inline { --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-success: var(--success); --color-destructive: var(--destructive); --color-primary-text: var( --primary-text, oklch(from var(--primary) var(--colored-text-lightness) c h) ); /* ... */ } ``` This means you can use them directly in Tailwind classes: ```tsx

Something went wrong.

``` ## Dark Mode Dark mode is toggled by adding the `.dark` class to the `` element (same convention as shadcn/ui). The same variable names are redefined with dark values: ```css .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --primary: oklch(0.56 0.2165 257.21); --colored-text-lightness: max(l, 0.63); /* ... */ } ``` Components automatically adapt -- no extra props needed. ## Typography | Token | Value | |---|---| | `--font-sans` | `"Manrope", sans-serif` | | `--font-mono` | `"Fira Code", monospace` | | `--font-heading` | not defined: titles inherit their font until you set it | Use in Tailwind: `font-sans`, `font-mono`, `font-heading`. Manrope and Fira Code are not shipped with the package. Load them in your app, or the browser falls back to its generic `sans-serif` and `monospace`. Tailwind's preflight applies `--font-sans` to the page, so importing the theme changes the page font. To keep your own font, set the token after the import: ```css @import "tailwindcss"; @import "chunks-ui/theme.css"; @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; } ``` The `Title` parts of Card, Dialog, Drawer, Popover and Empty use `font-heading`. Set `--font-heading` on `:root` or any element to style the titles inside. ## Easing Curves | Token | Value | Use case | |---|---|---| | `--ease-fluid` | `cubic-bezier(0.3, 0, 0, 1)` | General transitions | | `--ease-snappy` | `cubic-bezier(0.2, 0, 0, 1)` | Quick, responsive motion | Use in Tailwind: `ease-fluid`, `ease-snappy`. Most components that animate with a CSS transition use the `micro-interactions` class from `theme.css`. It transitions every property for 300ms on Tailwind's default curve, `--default-transition-timing-function` (`cubic-bezier(0.4, 0, 0.2, 1)` unless your theme sets another), except an outline focus ring: `outline` and `outline-offset` change at once, so a keyboard focus ring shows in its final colour, width and offset. A ring drawn with a box-shadow (`ring-*` utilities) still fades. A `duration-*` utility next to the class replaces all of the class's durations, and a `delay-*` utility delays every entry. On a bare element a `duration-*` utility brings the ring fade back, and a `delay-*` utility delays the ring. The parts of the library avoid that, as described below. With reduced motion the class turns the transition off, and no `transition-*` utility turns it back on. A few parts have their own transitions instead: the Accordion panel and chevron, the Progress indicator, and the fade-in of the ScrollArea scrollbar track. These turn their own transitions off with reduced motion, so a determinate Progress bar jumps to each new value. The indeterminate Progress pulse keeps running, because it is an essential status indicator. The ScrollArea thumb uses the class. The class sits in the `components` layer, so timing utilities in `className` override it on parts that keep their CSS transition, for example `