Make a surface.
131 systems for React 18.3 and 19. One stylesheet, optional fonts, native controls and purposeful motion. Mizu needs no Tailwind configuration and no provider for ordinary components.
Install
npm install mizu-ui "motion@^13"Run this command in your React application. Mizu supports React and React DOM 18.3 or 19, with Motion 12 or 13 as a peer dependency. Use the React versions your application already supplies, and keep an existing compatible Motion version. Then import the stylesheet and the components you need. The npm package and copied-source registry are alternative installation paths; you do not need both.
Install one component
npx shadcn@latest init 0xuser64bit/mizu/preset
npx shadcn@latest add 0xuser64bit/mizu/select-fieldshadcn’s init needs Tailwind and an import alias in your project. Plain init installs shadcn’s own theme into your global CSS; the Mizu preset writes components.json without it, so your global CSS, layout and dependencies stay as they are, and adds Mizu’s tokens and font files. Skip init if the project already has a components.json.
Mizu itself needs no Tailwind, and add needs only components.json and the alias. Without Tailwind (Vite, React Router, any React app), skip init and write the file yourself:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": { "config": "", "css": "", "baseColor": "stone", "cssVariables": true },
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"utils": "@/lib/utils",
"lib": "@/lib",
"hooks": "@/hooks"
}
}Map @/* to your source folder in tsconfig.json’s paths and in your bundler’s aliases. In a JavaScript project, set tsx to false and the CLI writes .jsx files. Set rsc to true for Next.js App Router. The full setup guide includes TypeScript and Vite alias examples.
Each catalog page has its own add command. The CLI copies the selected component and its source dependencies into your UI directory, with base.css, Mizu’s tokens, and the stylesheets that hold its rules. Each rule lives in one stylesheet, beside the module that renders it, or under shared/ when unrelated components render it, so components you add together never repeat CSS. Reveal and Presence, which only animate, and the MotionPreferences provider have no stylesheet of their own; base.css is all the CSS they bring. Motion is installed only for components that use it. Load the fonts as shown below.
import { SelectField } from "@/components/ui/mizu/select-field";Every component has its own item and path, its name in kebab case: DataInspector is data-inspector, and that file exports DataInspector and the types it takes. Use the UI alias configured in your components.json if yours differs from @/components/ui. The installed file imports that CSS itself. Wrap a surface in className="mizu-root" for the default theme, and edit the copied code to suit your app.
The installed files carry no "use client" of their own; each component’s source says whether it needs the browser. Mark, Badge, Kbd, Stat, Timeline and the other static pieces render as Server Components, with no client JavaScript. Every component page says which kind it is.
Keep data your Server Components read in a module without "use client", such as a data.ts. Exported from a client module, an array reaches server code as a client reference rather than an array, and .map fails while prerendering. Mizu’s own helpers (matchesQuery, squarify, FLAP_CHARACTERS) live in plain modules for the same reason.
Render a working interaction
This is the npm version. If you copied components instead, use the registry command and replacement imports immediately below it.
"use client";
import { useState } from "react";
import "mizu-ui/styles.css";
import "mizu-ui/fonts.css";
import { Button, TextField } from "mizu-ui";
export function App() {
const [draft, setDraft] = useState("");
const [projects, setProjects] = useState<string[]>([]);
return <div className="mizu-root" style={{ padding: 24 }}>
<form onSubmit={event => {
event.preventDefault();
if (!draft.trim()) return;
setProjects(items => [...items, draft.trim()]);
setDraft("");
}}>
<TextField label="Project name" required value={draft}
onChange={event => setDraft(event.target.value)} />
<Button type="submit">Create project</Button>
</form>
<ul>{projects.map((project, index) => <li key={index}>{project}</li>)}</ul>
</div>;
}For the registry version, add both components and replace the three npm imports above with these two imports. The copied modules import their required CSS; choose a font-loading method below separately.
npx shadcn@latest add 0xuser64bit/mizu/button 0xuser64bit/mizu/text-fieldimport { Button } from "@/components/ui/mizu/button";
import { TextField } from "@/components/ui/mizu/text-field";Import styles in your app entry or root layout. Set your document language and remove the browser’s default body margin.
Load the fonts
Mizu is set in Archivo, Instrument Serif and JetBrains Mono. Without them every component still works, in your system fonts, so a missing font is easy to miss. Choose one loading method and import it once in your app entry or root layout. For the npm package:
import "mizu-ui/fonts.css";For copied components, add the font item, then import from your UI directory:
npx shadcn@latest add 0xuser64bit/mizu/fontsimport "@/components/ui/mizu/fonts.css";With next/font, give the faces these variable names and set them on <html> or <body>. Mizu’s font tokens read them, so there is nothing to override. Omit the font CSS import when using this alternative. This root-layout example uses npm styles; registry users should omit the mizu-ui/styles.css import and keep the generated components’ own CSS imports.
import type { ReactNode } from "react";
import { Archivo, Instrument_Serif, JetBrains_Mono } from "next/font/google";
import "mizu-ui/styles.css";
const archivo = Archivo({
subsets: ["latin"],
axes: ["wdth"],
variable: "--font-archivo",
});
const serif = Instrument_Serif({
subsets: ["latin"],
weight: "400",
style: ["normal", "italic"],
variable: "--font-instrument-serif",
});
const mono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
});
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
const fonts = `${archivo.variable} ${serif.variable} ${mono.variable}`;
return (
<html lang="en" className={fonts}>
<body className="mizu-root">{children}</body>
</html>
);
}Keep axes: ["wdth"]: SoftType and the wide and narrow styles move along Archivo’s width axis. For other faces entirely, override --mizu-font-display, --mizu-font-serif and --mizu-font-mono on your surface.
Keep imports focused
import { TextField, Switch } from "mizu-ui/forms";
import { Progress, AsyncButton } from "mizu-ui/status";
import { MotionPreferences } from "mizu-ui/motion";All 14 families have subpaths. The root exports the same APIs. ESM, declarations, source maps and inspectable source ship together. Mizu never imports Next.js; the showcase uses the same implementations. CommonJS require is unsupported.
Choose a theme
<div className="mizu-root" data-theme="light">…</div>Dark is the default. Set data-theme on any ancestor, including <html>. Native dialogs keep the theme where they are rendered. With next-themes, write both the class Tailwind reads and the attribute Mizu reads:
<ThemeProvider attribute={["class", "data-theme"]}>Change the material
:root,
[data-theme] {
--mizu-accent: #2763c4;
--mizu-accent-fill: #2052a3;
}Mizu is drawn from --mizu-* tokens. They sit in a cascade layer, so your overrides win wherever your CSS loads. Name [data-theme] as above: each theme ancestor sets the colors again, and an override on :root alone stops there. Use [data-theme="light"] to change one theme. Foreground accent and filled-button accent are separate for legibility; check contrast when replacing colors.
<Button className="brand">Publish</Button>.brand {
--mizu-accent-fill: #2052a3;
}Set a token on one component to change only that one. States made from the token follow it: the hover darkens your color rather than returning to Mizu’s.
For properties without a token, pass className. Component rules sit outside any layer, mostly as one class: they beat Tailwind’s utilities, and a single class of yours wins only if it loads later. Your global CSS usually loads first. Add the component’s class to your selector (.mizu-btn.brand), or use Tailwind’s important modifier (px-5! in v4, !px-5 in v3). With the npm package and Tailwind v4, import the stylesheet from your CSS instead of JavaScript, into the components layer, and plain utilities win:
@import "tailwindcss";
@import "mizu-ui/styles.css" layer(components);Copied components are yours to change: edit their source and stylesheets directly.
Use your router’s links
Nav, Breadcrumbs, SideNav, BottomNav and ButtonLink render plain anchors, so each click loads a new document. Hand your router’s link component to LinkProvider once, near the root, and they navigate with it instead:
"use client";
import Link from "next/link";
import { LinkProvider } from "mizu-ui";
export function Providers({ children }: { children: React.ReactNode }) {
return <LinkProvider component={Link}>{children}</LinkProvider>;
}The component receives anchor props with a string href. React Router’s Link takes to, so wrap it: ({ href, ...props }) => <Link to={href} {...props} />.
Use real states
Native fields accept standard form attributes and controlled or default values. State systems expose explicit values and callbacks. AsyncButton and AsyncForm follow your promise, keep failure visible and provide AbortSignal on unmount; your service must honor that signal. FileDropzone returns validated files to your code. It does not upload them.
Motion and keyboard
<MotionPreferences reduced={true}>
<YourInterface />
</MotionPreferences>With no override, the operating system’s motion preference applies. The provider additionally stops Mizu CSS and canvas movement. Continuous tickers include a pause control; native inputs keep platform keyboard behavior. Read each component’s notes for its particular interactions and limits.
npx shadcn@latest add 0xuser64bit/mizu/motion-preferencesFrom the registry, the provider is its own item. Import it from @/components/ui/mizu/motion-preferences; it governs every Mizu component installed alongside it.
Update copied components
add asks before it replaces a file that differs from the registry’s, even with --yes, and skips files that match. Components share base.css and the stylesheets under shared/, so adding one can ask about those; answer no to keep your edits. To take a newer Mizu, add what you installed again with --overwrite. It replaces your edits to those files, so commit first and review the diff:
npx shadcn@latest add --overwrite 0xuser64bit/mizu/select-fieldRepeat your original add commands for the other installed items or blocks, reviewing shared source and CSS too. The changelog says what changed.
Inspect and copy
Every component page has a live preview, compiled typed usage, API notes and a source link. Source ships under node_modules/mizu-ui/src. Copy its relative dependencies too; imports use .tsx/.ts extensions and the package build rewrites them to .js. Use TypeScript 5.7+ with allowImportingTsExtensions/rewriteRelativeImportExtensions if compiling copied source, or adapt extensions to your build system. Keep the stylesheet and MIT license.