Introduction
Tactile, dark-first React components on Radix UI and Tailwind CSS v4, installed from npm and updated like any other package.
A package you update#
Copy-paste kits such as shadcn/ui put each component's source in your repository. You own every line, and a fix made upstream reaches an app only when someone copies it in again.
cennge-ui aims for that kind of finish, but ships the way Mantine does: as a regular npm package. The components live in node_modules, a fix is made once in the library, and npm update brings it to every app that uses it. You still restyle them as much as you like, without forking: every colour, surface and radius is a --cennge-* CSS variable, className is merged with tailwind-merge so your classes win, and components with inner parts, such as Input and Dialog, take classes for them through classNames.
What it looks like#
A Card, an Input, a Select, a Switch, a Badge and a Button, as they come. Change the version and publish: the button waits, then a toast confirms. Switch the site's theme and accent and the card follows.
payload-cache
New release
import { useState } from "react";
import { Badge, Button, Card, CardBody, CardFooter, CardHeader, Input, Select, SelectItem, Switch, toast } from "cennge-ui";
export default function ReleaseCard() {
const [version, setVersion] = useState("1.4.0");
const [tag, setTag] = useState<string | null>("latest");
const [isPublishing, setPublishing] = useState(false);
async function publish() {
setPublishing(true);
// Stands in for the real request.
await new Promise((resolve) => setTimeout(resolve, 1200));
setPublishing(false);
toast.success("Published", { description: `payload-cache@${version} is on npm as ${tag}` });
}
return (
<Card className="w-full max-w-md">
<CardHeader className="items-start justify-between">
<div className="min-w-0">
<h3 className="font-semibold">payload-cache</h3>
<p className="text-sm text-muted">New release</p>
</div>
<Badge color="success" variant="dot">
CI passing
</Badge>
</CardHeader>
<CardBody className="gap-4">
<div className="grid gap-3 sm:grid-cols-2">
<Input variant="faded" label="Version" value={version} onValueChange={setVersion} />
<Select variant="faded" label="Tag" value={tag} onValueChange={setTag}>
<SelectItem value="latest">latest</SelectItem>
<SelectItem value="next">next</SelectItem>
</Select>
</div>
<Switch defaultSelected>Publish with provenance</Switch>
</CardBody>
<CardFooter className="justify-end">
<Button color="primary" variant="shadow" isLoading={isPublishing} isDisabled={!version || !tag} onClick={publish}>
{isPublishing ? "Publishing" : "Publish"}
</Button>
</CardFooter>
</Card>
);
}What is inside#
These docs run to 74 pages in 11 families. The first 7, under Getting started, are guides like this one; the other 10 families cover the components:
- Actions, 2 pages. Buttons and grouped toggles.
- Inputs, 15 pages. Fields that work controlled and uncontrolled, inside <Form> and with react-hook-form.
- Dates and times, 4 pages. ISO strings in and out, Intl for names and order, no date library.
- Overlays, 10 pages. Nested in each other, opened by touch and by keyboard, tested in every engine.
- Navigation, 6 pages. Getting around an app: menus, sidebars, steps and pages.
- Data display, 10 pages. Tables, trees, long lists and the small pieces inside them.
- Feedback, 6 pages. Status, progress and the moments before content arrives.
- Layout, 4 pages. Surfaces and structure.
- Charts, 6 pages. cennge-ui/charts: own SVG, no chart library, palette checked for colour blindness.
- Map, 4 pages. cennge-ui/map: MapLibre, loaded only when a map mounts, themed by your tokens.
Everything above comes from the main entry, cennge-ui, except charts and maps, which have their own: cennge-ui/charts and cennge-ui/map. In the ESM build each component is a chunk of its own, so a page pays only for what it imports; every component's page gives the gzip size of its import in its field marks.
What it is built on#
The package has four dependencies:
radix-ui, for behaviour, focus management and keyboard handling. Where Radix has no primitive (the calendar, the tree, the ⌘K palette), the keyboard follows the WAI-ARIA Authoring Practices.tailwind-variants,tailwind-mergeandclsx, for class names: each component's variants, and yourclassNamewinning over them.
It also expects what your app already has: react and react-dom 17, 18 or 19, and tailwindcss 4. maplibre-gl 6 is needed only for maps, and is loaded when the first map mounts.
What it leaves out#
- No animation library such as framer-motion. Motion is plain CSS, keyframes and transitions, and respects
prefers-reduced-motion. - No CSS-in-JS. Styles are Tailwind classes and the theme is CSS variables, so retheming is a change to your stylesheet, with nothing to configure in JavaScript.
- No chart library. Charts are drawn in SVG by cennge-ui itself, with a palette checked for colour blindness.
- No date library. Dates are ISO strings such as
"2026-10-19"; names of months and days, the first day of the week and the order of a typed date come fromIntl. - No carousel or virtualization package either: the Carousel runs on native scroll snapping, and long lists use the library's own virtualizer.
Fewer packages underneath means fewer upstream releases that can break a component you did not touch.
How it is checked#
- Unit tests with Vitest and Testing Library, for logic, roles and labels.
- End-to-end tests with Playwright in Chromium, Firefox and WebKit, on desktop and phone viewports: real keyboard, mouse, touch, file choosers and drag events.
- axe-core against WCAG 2.2 AA on every fixture, in the dark and the light theme.
- Any error or warning in the browser console fails a test.
- A Next.js App Router app, built on the packed package, renders the whole playground on the server and hydrates it in every browser.
- CI runs the unit tests on React 17 and 18 as well; the other jobs run React 19.
Where a component has end-to-end specs, the field marks on its page link to them.
Status#
cennge-ui is in early development, at version 0.x, under the MIT licence. Every release lists its changes in the changelog: read it before you update.
Next#
- Installation: the package, two CSS imports, and a first component.
- Theming: tokens, surfaces, colour roles and the accent presets.
- Dark mode: the stored theme on the page before the first paint.