Skip to content

Changelog

Every release,and why it happened.

Read from the package’s own CHANGELOG.md at build time, so this page is the release notes rather than a retelling of them. Versions follow semver: a major is a break, a minor adds, a patch fixes.

Latest1.3.0PackagekipuiReleases5

1.3.0

Minor

Add a subpath for every component, and fix a webpack build that dragged the library in.

def4b18

Shipping a file per module in 1.2.0 fixed Turbopack — one <Button/> went from +77.3 kB gzip to +9.8 kB — but it moved the problem onto webpack, where the same page went the other way: +10.4 kB to +76.5 kB, framer-motion back in the bundle, Select.js and Calendar.js in a page that renders neither.

The barrel is the reason. Without a directive of its own it is a server module that imports twenty-six client modules, and Next's webpack builder turns every "use client" module it finds in the server graph into a client entry whether the export is used or not. Turbopack does not do this.

Putting "use client" back on the barrel fixes webpack and costs 67 kB gzip on Turbopack, where the barrel then becomes one client entry carrying all seventy-five exports — the 1.1.0 failure again. Measured, not assumed. So the barrel keeps no directive, and there are two ways past it instead:

  • `kipui/button`, `kipui/table`, `kipui/radio-group` — every component now has its own subpath, named in kebab-case after the component. Importing one reaches one file and never touches the barrel, which fixes webpack with no configuration at all: 795.7 kB raw down to 574.2 kB, identical to what the barrel directive bought, with no cost under Turbopack.
  • `experimental.optimizePackageImports: ["kipui"]` — for anyone who would rather keep importing from the root. It could not work in 1.1.0, when dist was one pre-bundled file with no re-exports to rewrite; now that the barrel is a real barrel, it does, to the same number.

The root import stays the recommended one, and is unchanged for Turbopack users. typesVersions maps the subpaths for projects still on TypeScript's legacy node resolution, which cannot see an exports map. publint and @arethetypeswrong/cli are clean across all twenty-nine entry points.

Also moved Tooltip's "use client" above the eslint-disable comment it sat under. It worked — a directive may follow comments — but it depended on that staying true through every build step, and nothing would have failed loudly if it stopped.

The installation docs now cover all of this, including narrowing @source to the components you use, which cuts the generated CSS from about 98 kB to 42 kB in exchange for a line to maintain per component.

1.2.0

Minor

Ship a file per module instead of one bundle. Turbopack now shakes the package.

7077d90

1.1.0 made the bundle shakeable and measured it with esbuild, which agreed with webpack: one <Button/> cost 10 kB gzip. Turbopack — the default in Next 16 — disagreed. A differential production build of a page containing nothing but <Button/> put kipui at +77.3 kB gzip, framer-motion included, against +10.4 kB for the same page built with --webpack. Grepping the chunk found pagination-ellipsis, tabs-indicator and alert-description: effectively all 75 exports.

The cause was the shape of the output, not the code. dist/index.js was a single 226 KB file whose first line was "use client", and Turbopack treats a bundled client module as one atom — sideEffects and /* @__PURE__ */ do not get a say. experimental.optimizePackageImports cannot help either, because it rewrites barrel re-exports and this was a pre-bundled file, not a barrel.

So the package no longer bundles. Every file under src compiles to its own file under dist, and the import graph survives into the output. A consumer that never names Toast never reaches the file that imports framer-motion — no bundler has to prove the import is unused, because it never sees it.

Measured the same way, on Turbopack: +77.3 kB gzip → +9.8 kB. framer-motion is absent from the client bundle entirely. Webpack is unchanged at ~10 kB, so both bundlers now agree.

The "use client" boundary moves with it. It was one banner over the whole package; it is now one directive per file, on the components that actually need it. Table, Breadcrumb, Kbd, Label, Field, Separator and Skeleton have none, so they render as Server Components and contribute zero client JavaScript — verified against a server page that renders a Table and a Kbd and ships neither.

Two things this changes for consumers:

  • The `@source` line should now point at `node_modules/kipui/dist/esm`. The old path still works but scans the CJS mirror as well, doubling the work for the same output. The install and theming docs are updated.
  • dist is now dist/esm and dist/cjs, wired through exports. Nothing imports those paths directly — the entry point is still kipui — but anything reaching past exports into dist/index.js will need updating.

publint and @arethetypeswrong/cli are clean on all four resolution modes. Package size is 98 KB packed.

1.1.0

Minor

Make the bundle tree-shakeable. Importing one component no longer pulls in the library.

2eafd8b

Measured in a bare Next 16 app importing nothing but Button, the whole package landed in the client bundle: 66.16 kB brotli against 73.43 kB for import *. One component cost 90% of everything, framer-motion included, even though Button has never used it.

Two module-level constructs were the cause, and neither is visible from the source:

  • X.displayName = "X" is a property assignment on a module-level binding. Bundlers must treat it as a side effect, so the component it names can never be dropped — and it holds every module it references in with it. The name now lives on the render function (forwardRef(function Badge(…))), which React DevTools reads the same way. Where the component was already a function X declaration the line was pure redundancy, because React falls back to fn.name.
  • forwardRef(…) and createContext(…) are calls of unknown purity at module level. Both now carry an explicit /* @__PURE__ */.

Button alone is now 9.31 kB brotli, down from 66.16 kB. import * is unchanged at 73 kB, as it should be — nothing was removed, it simply became possible to leave out. The annotation alone did nothing; the displayName assignments were the real anchor.

Sourcemaps are no longer built or published. The two of them were 972 KB of a 1.5 MB install — the package is now 555 KB unpacked, 122 KB packed — to debug a library whose source is public. Excluding them from files instead would have left a sourceMappingURL pointing at a file nobody ships.

Also fixed the exports map, which advertised the ESM .d.ts to require() consumers while serving them CJS — @arethetypeswrong/cli flagged it as masquerading as ESM. types now sits inside the import and require conditions, with ./dist/index.d.cts wired to the CJS build it was always generated for. publint and attw are clean on all four resolution modes.

Drop framer-motion from Badge and RadioGroup; their animations are now CSS.

b37ebb3

framer-motion is a real dependency, and because the package builds to one bundled file, importing any component pulls the whole runtime into the consumer's app. Badge was the worst case: a static label whose only motion was a mount fade, costing every consumer the entire animation library. RadioGroup was close behind — a scale and an opacity on a mark that is at most 24px across.

Both now animate with CSS, using tokens that already ship in theme.css:

  • Badge mounts with the animate-scale-in preset instead of motion.span. The class sits first in the list, so animate-none from the caller overrides it, and the duration scales with the --motion knob — neither of which the framer version did.
  • RadioGroup transitions the mark on ease-bounce, which carries the overshoot the spring had. The check appearance draws its tick with pathLength={1} and a stroke-dashoffset transition, the CSS equivalent of framer's pathLength.
  • Both now respect prefers-reduced-motion via motion-reduce:. The framer versions ignored it.

Two behaviour changes:

  • Badge no longer plays an exit animation. It never played one on its own — it required an <AnimatePresence> around the list in the consumer — but that path is gone. Removal is now immediate.
  • Badge no longer renders a wrapper <span>. The element carrying className, ref and the rest of the props is unchanged; there is simply one fewer element around it. Selectors written against that wrapper need updating.

Neither component imports framer-motion anymore, so once the package ships per-file output, an app that uses only these two will not download it at all.

Add --success-foreground, --warning-foreground and --info-foreground, and use them.

2750e5c

The three state fills had no text token, so nine components hardcoded text-white or text-black on top of them. Measured against the palettes that ship, white on --success is 3.3:1 and white on --info is 3.7:1 — both below the 4.5:1 WCAG AA threshold for body text, and neither fixable from a theme, because there was no variable to change. An app with a pale success colour had no way to make its own buttons legible.

All five palettes now declare the three tokens in both modes, and Alert, Badge, Button, Checkbox, RadioGroup, Switch, Toast and Tooltip read them. The shipped values measure 4.7:1 to 9.6:1.

Also removed from the same components: text-gray-700 dark:text-gray-200 in Field and Label (now text-foreground), text-red-500 and border-red-500 (now --destructive), the literal white/5 glass on the capsule Tabs variant (now tinted from --foreground, so it reads on a light theme), and the white/30 button ripple (now currentColor).

Two literal colours are kept deliberately and commented: the dialog scrim, which exists to darken what is behind it, and the tick colour when Checkbox is given an arbitrary color prop, where there is no token to pair with.

1.0.0

Major

kipui is now the component library itself, not a CLI that copies it.

b84bf72

Up to 0.1.0 this package was a tool: kipui add table fetched a registry document and wrote the component's source into your repository. That is gone — there is no kipui binary, no kipui add, no components.json and no registry to fetch from. Installing the package now gives you the components.

npm install kipui
/* app/globals.css */
@import "tailwindcss";
@import "kipui/theme.css";
@source "../node_modules/kipui/dist";
import { Button, Table, Dialog } from "kipui";

The @source line is not optional. Tailwind only generates a utility it has seen written down, and this package's class names live in node_modules, which it does not scan by default.

If you were using the CLI, nothing breaks on its own: the files it already wrote into your repository are yours and keep working. Upgrading to 1.0.0 gives you a package with no binary, so a kipui add in a script will fail — either stay on 0.1.0, or delete the copied files and import from the package instead.

All twenty-two components moved across unchanged apart from two things a package cannot assume about its consumer:

  • BreadcrumbLink no longer imports next/link. It renders a plain <a> by default and takes an as prop for your router's link component.
  • The font stacks read --kip-font-sans, --kip-font-mono and --kip-font-display instead of this repository's own --font-geist-* variables, each with a system fallback so an app that sets none still renders in a sans-serif rather than invalidating the declaration.

0.1.0

MinorPatch

First release of the CLI.

75fb7a4

kipui init writes a components.json describing where this project keeps its components and what its imports look like. kipui add <name> reads the registry, resolves everything the component imports — other components, shared helpers, hooks — and writes each file to the directory that config names, rewriting the @/ imports inside it to match. kipui list shows what is available and kipui diff compares your copy against the registry's, which is the question worth asking about code you were handed rather than installed.

Nothing is added to node_modules. The files are yours once they land, and the CLI has no opinion about what you do to them afterwards.

Corrected the documented API for several components.

31f9351

Tooltip's props table and usage snippet described an older single-component API — content, placement, delay, disabled — none of which the component accepts. Anyone following it wrote code that did not compile. It now documents the real Tooltip / TooltipTrigger / TooltipContent composition.

Thirty-eight props across Checkbox, RadioGroup, Switch, Select, Calendar, Table, Avatar, Dialog, Tabs and Pagination existed but were undocumented. They are now in their tables, and a test fails if any table drifts from the type again.

Point the default registry at the deployed site, and add a KIPUI_REGISTRY override.

3f1cffe

The default was https://kipui.dev/r, a domain nobody owns, so the CLI could not fetch anything. It now points at where the site actually is.

KIPUI_REGISTRY overrides both that default and whatever components.json says. The default is baked into each published version, so a reader on an old CLI would otherwise keep asking the old host forever — this is what makes the site movable without stranding them.

Tabs no longer requires defaultValue when value is supplied.

cdaacfb

It was typed as required, so a fully controlled Tabs had to pass an initial tab it would never read. The documentation site's own package-manager tabs were passing both, which is how the mistake stayed invisible — the workaround looked like ordinary usage.

© 2026 Kipui — MIT licensed

all systems nominal