Installation
The three peer dependencies, the two lines your stylesheet needs, and the import that follows.
Requirements
Three peer dependencies. Your package manager will tell you if one is missing, which is the difference between a package and a folder of copied files.
| Requirement | Version | Why |
|---|---|---|
| react | 19 | Hooks and function components only. |
| react-dom | 19 | Dialog, Select, Tooltip and Toast render through a portal. |
| tailwindcss | 4 | The tokens are declared with @theme and @custom-variant, both v4 syntax. |
Tailwind v4 specifically
@theme, @source and @custom-variant, none of which exist in v3 — there is no config-file equivalent to fall back to.Install the package
One package, every component. There is nothing to add per component after this.
npm install kipuiTypeScript types are bundled — there is no @types package to install alongside it. The build ships ESM and CJS, so it works under a bundler and under plain require.
Wire up the stylesheet
One line, once, in the stylesheet that already imports Tailwind. Skip it and the components render with their markup intact and none of their styling.
/* app/globals.css */
@import "tailwindcss";
/* The tokens every component is written against — and, inside it, the
@source that points Tailwind at the package's class names. */
@import "kipui/theme.css";- 01
The import brings the variables
Five palettes, the radius and shadow scales, the z-index ladder, and the keyframesButtonandSkeletonanimate. It declares no component styles — those are Tailwind utilities inside the JavaScript. - 02
It also brings the classes
Tailwind only generates a utility it has seen written down, and the library's class names live innode_modules, which it does not scan by default.theme.csscarries its own@sourcefor exactly that, resolved relative to itself. You used to write that path by hand and it was the single most common reason a fresh install looked unstyled. - 03
Order matters
After@import "tailwindcss", because the variables it declares are what Tailwind builds utilities from — and before your own overrides, so yours win. - 04
Dark mode is a class
Putdarkon<html>and every component follows. Nothing in a component reads or writes it, so an existing theme switcher keeps working as-is — and if you do not have one, the package ships one.
Use your own palette
The five that ship are defaults, not a commitment. Redeclare the variables after the import and every component follows — the utilities resolve through var(), so an override reaches all of them without touching a component. Redeclare only what you are changing: anything you leave alone keeps tracking the package, and picks up tokens added in later releases.
/* After the import, so yours wins. Only the tokens you are changing. */
:root {
--primary: oklch(0.70 0.166 52);
--primary-foreground: oklch(0.99 0.004 90);
--background: oklch(0.97 0.006 95);
--foreground: oklch(0.18 0.012 70);
--ring: oklch(0.70 0.166 52);
}
.dark {
--primary: oklch(0.72 0.17 55);
--background: oklch(0.16 0.008 70);
--foreground: oklch(0.96 0.004 90);
--ring: oklch(0.72 0.17 55);
}What those nine lines do
The panes below are real components with exactly that block applied, nothing else. Both modes are shown together because an override has to be written twice: a brand colour tuned against white is almost always too dark to read on a dark background.
light
Heads up
Tinted surfaces follow the same variables.
dark
Heads up
Tinted surfaces follow the same variables.
Or generate it
You can narrow it, at a cost
@source scans every component, so Tailwind generates every class the library can produce — about 98 kB of CSS, 17 kB gzipped, whether you render one component or all of them. Opting out and naming directories instead cuts that to roughly 42 kB. The catch is that it stops being a thing you set once: every component you add later needs its own line, and forgetting one shows up as an unstyled component rather than an error. Start with the default; narrow it if the CSS becomes worth the upkeep./* Only if the generated CSS becomes worth the upkeep. Paths are
relative to kipui's own theme.css, so they start at dist/esm. */
@import "kipui/theme.css";
@source not "kipui/dist/esm";
@source "../node_modules/kipui/dist/esm/components/button";
@source "../node_modules/kipui/dist/esm/lib";
@source "../node_modules/kipui/dist/esm/icons";Use a component
Import by name from the package root. Compound components export every part on its own, so you assemble the markup rather than passing a schema.
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "kipui";
export function Contacts() {
return (
<Table hoverable gridLines>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Ada Lovelace</TableCell>
<TableCell>ada@example.com</TableCell>
</TableRow>
</TableBody>
</Table>
);
}Or import one component directly
kipui/button, kipui/table, kipui/radio-group, named after the component in kebab-case. The root import is the one to reach for; the subpaths exist for when you want the barrel out of the picture entirely, which is what a webpack build needs if you would rather not set optimizePackageImports. Either way the package ships one file per component, so naming Button is what reaches Button.js.Frameworks
Nothing in the package imports a router or a data layer, so there is no per-framework install — only the two things any of them has to give you.
Anywhere React 19 and Tailwind v4 both work, the components work unmodified: Next.js, React Router, Remix, Astro, Vite and TanStack Start have all been used as-is. What differs between them is only where your global stylesheet lives, and therefore what the @source path has to be.
App Router: the boundary is drawn per component
"use client", and the ones that do not need it do not have it — Table, Breadcrumb, Kbd, Label, Field, Separator and Skeleton render as Server Components and ship no client JavaScript at all. Import straight from a server component; adding a directive of your own pushes the boundary further up than it has to go, and pulls those seven across it.Building with webpack? Add one line
"use client" module it finds in the server graph into a client entry, whether or not the export is used — so importing one component from the package root drags the rest in. Turbopack, the default since Next 16, does not. If you still build with --webpack, set experimental.optimizePackageImports: ["kipui"] in next.config.ts: it rewrites the barrel import into a direct one before webpack sees it. Measured on a page holding a single button, that is 66 kB gzip. It costs nothing under Turbopack.One component takes a router prop
BreadcrumbLink renders a plain <a> by default, which navigates correctly everywhere and full-page-reloads inside a SPA. Pass as={Link} to hand it your router's link component and get client-side navigation back.