How to install and use Meridian
Meridian is the design system this site is built with. This page shows how to install it, add components, change the theme and use the ink background. Read the credit section first. It is the one thing Meridian asks in return.
What Meridian is§
Meridian is a design system for React 19 projects that use Tailwind CSS v4. It has 76 interface components, such as buttons, dialogs, data grids and forms, 6 chart pieces, and the moving ink background.
One token file holds every colour, shadow, radius and motion setting. It has a light theme and a dark theme. Components use those tokens and never hard-coded colours, so changing the file changes everything.
You copy components into your own project, so you own the code and can change it. It works the way shadcn/ui does.
Credit is required§
Meridian's license is MIT with one extra rule. Any site that uses Meridian must show a visible credit that reads "Built with Meridian by Nathan Zhou" or "Built with Meridian by nathanz.cloud", linked to https://nathanz.cloud. A line in the footer is enough.
<footer>Built with Meridian by <a href="https://nathanz.cloud">nathanz.cloud</a></footer>If you also use the ink background, show "Background by Nathan Zhou" or "Background by nathanz.cloud" as well, linked to the same address. Both lines can sit together.
<footer>
Built with Meridian by <a href="https://nathanz.cloud">nathanz.cloud</a>.
Background by <a href="https://nathanz.cloud">nathanz.cloud</a>.
</footer>Keep the credit where visitors can see it. Do not hide it or leave it only in a comment.
What you need§
- Node.js.
- A React 19 project with Tailwind CSS v4 already set up.
To start one, run npx create-next-app@latest, or use Vite with @tailwindcss/vite.
npx create-next-app@latestSet up§
Run one command in your project folder.
npx @nathanzhou/meridian initIt asks a few questions, then does this:
- Checks that you have React 19 and Tailwind v4.
- Adds Meridian's tokens as
meridian-tokens.cssnext to your global CSS and imports it. - Uses your
@/import alias if you have one. If not, it uses relative imports. - Installs
clsxandtailwind-merge, and adds@nathanzhou/meridianas a dev dependency. - On Vite, offers to add the three fonts to
index.html. On Next.js, prints thenext/fontsnippet. - Offers to add a skill, so coding agents such as Claude Code know how to use Meridian. It goes in
.claude/skills/meridian/. - Saves your answers in
meridian.json.
Add --yes to take every default without questions.
npx @nathanzhou/meridian init --yesAdd components§
See everything that is available:
npx meridian listAdd one. This copies the component and anything it needs into src/components/ui/, and installs the npm packages it uses.
npx meridian add buttonThen import it from your own project:
import { Button } from "@/components/ui/button";
export default function Page() {
return <Button>Save</Button>;
}You can add several at once:
npx meridian add card dialog tabsEach component has a README with its props and examples. They come with the package and are linked on its npm page.
Change the theme§
Every colour is a CSS variable in meridian-tokens.css. The light values are in :root and the dark values are in .dark. Change them there and every component follows.
In your own code, use token classes such as bg-primary, text-muted-foreground and border-border. Do not write hex colours. That is what keeps one file in charge.
<p className="border-b border-border text-muted-foreground">Quiet text</p>
<button className="bg-primary">Main action</button>Dark mode§
Add class="dark" to the <html> element and the dark theme turns on.
<html lang="en" class="dark">On Next.js, next-themes does this for you. Use attribute="class".
import { ThemeProvider } from "next-themes";
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>Fonts§
Meridian is designed with three fonts: Inter for text, Instrument Serif for headings, and JetBrains Mono for numbers and code. They are mapped through --font-inter, --font-instrument and --font-jbmono.
This is the Next.js setup. Put the variables on the <html> element.
import { Inter, Instrument_Serif, JetBrains_Mono } from "next/font/google";
const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
const instrument = Instrument_Serif({ variable: "--font-instrument", subsets: ["latin"], weight: "400" });
const jbmono = JetBrains_Mono({ variable: "--font-jbmono", subsets: ["latin"] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${instrument.variable} ${jbmono.variable}`}>
<body>{children}</body>
</html>
);
}Fonts are optional. Without them, the system fonts are used.
The ink background§
The ink background is the moving background on this site: ink marbling in paper-coloured water, drawn with WebGL. A canvas sits behind your page. By default the middle stays plain so text is easy to read, and the ink gathers toward the edges. If a visitor asks their device for reduced motion, it shows one still frame.
Showing it needs the "Background by" credit from the credit section above.
With Meridian
Add the component:
npx meridian add ink-backgroundThen use it:
import { InkBackground } from "@/components/ui/ink-background";
export default function Page() {
return (
<>
<InkBackground />
<main>Your content</main>
<footer>Background by <a href="https://nathanz.cloud">nathanz.cloud</a></footer>
</>
);
}This version takes its colours from Meridian's tokens, so it follows light and dark with nothing to set up.
Without Meridian
The standalone package @nathanzhou/ink-background has no dependencies. React is only needed for the React component.
npm i @nathanzhou/ink-backgroundIn React:
import { InkBackground } from "@nathanzhou/ink-background/react";
export default function Page() {
return (
<>
<InkBackground />
<main>Your content</main>
<footer>Background by <a href="https://nathanz.cloud">nathanz.cloud</a></footer>
</>
);
}In plain JavaScript or with a bundler, give createInk a canvas that covers whatever the ink should fill:
<canvas id="ink" style="position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1"></canvas>
<script type="module">
import { createInk } from "@nathanzhou/ink-background";
const ink = createInk(document.getElementById("ink"), { presence: "calm" });
// later: ink.setColors({ body: "teal" }); ink.setPresence("full"); ink.destroy();
</script>createInk returns an object with these:
supportedisfalsewhen WebGL is not available. The canvas is then left alone.setColors(colors)changes the colours without restarting the ink.setPresence(presence)switches between calm and full.destroy()stops drawing and removes everything it added.
On a plain HTML page you can skip the install and import it from a CDN. This pins version 0.1.0:
<canvas id="ink" style="position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1"></canvas>
<script type="module">
import { createInk } from "https://cdn.jsdelivr.net/npm/@nathanzhou/[email protected]/dist/index.js";
createInk(document.getElementById("ink"));
</script>
<footer>Background by <a href="https://nathanz.cloud">nathanz.cloud</a></footer>Options
createInk takes presence, colors and still. The React component takes those too, and the rest.
| Option | Default | What it does |
|---|---|---|
| presence | "calm" | "calm" keeps the centre plain for text. "full" puts ink everywhere. |
| colors | light palette | Any of paper, wash, body, deep and core, as any CSS colour, including var(--token). |
| still | false | Draw one still frame and do not animate. |
| position | "fixed" | React only. "absolute" fills the nearest positioned parent instead of the screen. |
| zIndex | -1 | React only. Where the layer sits in the stack. |
| className, style | none | React only. Added to the layer. |
The five colours are:
| Colour | What it colours |
|---|---|
| paper | The water, and everything the ink has not reached. |
| wash | The faint halo at the edge of the ink. |
| body | The ink itself. |
| deep | Ink that has dried darker. |
| core | The darkest centres. |
Any colour you leave out comes from the light palette. For a dark page, pass the dark palette. Colours written as var(--token) are read again when your theme changes, so a theme switch needs no extra code.
import { createInk, PALETTES } from "@nathanzhou/ink-background";
createInk(canvas, { colors: PALETTES.dark });If something looks wrong
- I cannot see it. Something opaque covers it. Look for a background colour on
body,htmlor a wrapper. Remove it, or move that colour intopaper. - It is barely there. That is the calm setting. Use
presence="full"to put ink everywhere. - It does not move. The device asks for reduced motion, so it shows one still frame. That is on purpose.
- I see a soft gradient, not ink. WebGL is not available in that browser. The React version falls back to a gradient in the same colours.
Using it with coding agents§
If you let init add the skill, your coding agent knows how Meridian works. Ask for what you want in plain words, for example:
Add a settings page using Meridian components.The skill tells the agent to run npx meridian add for each component, to use the tokens, and to keep the credit on the page.