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.

RequirementVersionWhy
react19Hooks and function components only.
react-dom19Dialog, Select, Tooltip and Toast render through a portal.
tailwindcss4The tokens are declared with @theme and @custom-variant, both v4 syntax.

Tailwind v4 specifically

Not v3. The token layer is built on @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 kipui

TypeScript 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
/* 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";
  1. 01

    The import brings the variables

    Five palettes, the radius and shadow scales, the z-index ladder, and the keyframes Button and Skeleton animate. It declares no component styles — those are Tailwind utilities inside the JavaScript.
  2. 02

    It also brings the classes

    Tailwind only generates a utility it has seen written down, and the library's class names live in node_modules, which it does not scan by default. theme.css carries its own @source for 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.
  3. 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.
  4. 04

    Dark mode is a class

    Put dark on <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.

app/globals.css
/* 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

Beta
Heads up

Tinted surfaces follow the same variables.

dark

Beta
Heads up

Tinted surfaces follow the same variables.

Or generate it

The theme generator takes one brand colour, derives every token for both modes with the contrast checked, previews it on real components, and hands back a block exactly like the one above — only longer, because it fills in the tokens this example leaves alone.

You can narrow it, at a cost

The bundled @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.
app/globals.css
/* 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.

app/contacts/page.tsx
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

Every component also has its own path — 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

Each component carries its own "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

Next's webpack builder turns every "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.