Skip to content
Nathan Zhou

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.

html
<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.

html
<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§

To start one, run npx create-next-app@latest, or use Vite with @tailwindcss/vite.

sh
npx create-next-app@latest

Set up§

Run one command in your project folder.

sh
npx @nathanzhou/meridian init

It asks a few questions, then does this:

Add --yes to take every default without questions.

sh
npx @nathanzhou/meridian init --yes

Add components§

See everything that is available:

sh
npx meridian list

Add one. This copies the component and anything it needs into src/components/ui/, and installs the npm packages it uses.

sh
npx meridian add button

Then import it from your own project:

tsx
import { Button } from "@/components/ui/button";

export default function Page() {
  return <Button>Save</Button>;
}

You can add several at once:

sh
npx meridian add card dialog tabs

Each 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.

tsx
<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
<html lang="en" class="dark">

On Next.js, next-themes does this for you. Use attribute="class".

tsx
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.

tsx
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:

sh
npx meridian add ink-background

Then use it:

tsx
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.

sh
npm i @nathanzhou/ink-background

In React:

tsx
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:

html
<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:

On a plain HTML page you can skip the install and import it from a CDN. This pins version 0.1.0:

html
<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.

OptionDefaultWhat it does
presence"calm""calm" keeps the centre plain for text. "full" puts ink everywhere.
colorslight paletteAny of paper, wash, body, deep and core, as any CSS colour, including var(--token).
stillfalseDraw one still frame and do not animate.
position"fixed"React only. "absolute" fills the nearest positioned parent instead of the screen.
zIndex-1React only. Where the layer sits in the stack.
className, stylenoneReact only. Added to the layer.

The five colours are:

ColourWhat it colours
paperThe water, and everything the ink has not reached.
washThe faint halo at the edge of the ink.
bodyThe ink itself.
deepInk that has dried darker.
coreThe 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.

ts
import { createInk, PALETTES } from "@nathanzhou/ink-background";

createInk(canvas, { colors: PALETTES.dark });

If something looks wrong

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:

text
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.

← Meridian