Next.js

Merid works in the App Router without configuration. This site is itself a Next.js 16 app built with it.

npx create-next-app@latest my-app
cd my-app
npx @meridui/cli init

init installs @meridui/react, imports the stylesheet in app/layout.tsx and fixes the template's globals.css (see below). Every change is shown as a diff before it is written.

Set up

// app/layout.tsx
import type { Metadata, Viewport } from "next";
import "@meridui/react/styles.css";
import "./globals.css"; // your CSS after Merid's (with Tailwind, the other way round; see below)

export const metadata: Metadata = {
  title: { default: "Acme", template: "%s — Acme" },
};

export const viewport: Viewport = {
  // Matches --mrd-bg in light and dark so mobile browser chrome blends in.
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "#ffffff" },
    { media: "(prefers-color-scheme: dark)", color: "#0b0d12" },
  ],
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

The base layer applies typography, background and focus styles to the document at zero specificity; no class is needed. No transpilePackages entry is needed. In server component files use the flat part names (DialogRoot, TabsList, …) and asChild for router links; see Server components.

The template's globals.css

create-next-app writes an app/globals.css whose rules are not in a cascade layer, so they beat Merid's.

With Tailwind (the default), two things need changing. The body rule sets Arial and the template's colours; remove it. And Tailwind's reset, Preflight, is in a layer named base: when Merid's stylesheet loads first, that layer ends up above Merid's and strips the padding, borders and background from buttons and inputs. Put the layer order at the top of globals.css and import globals.css before Merid:

/* app/globals.css */
@layer theme, base, merid, components, utilities;

@import "tailwindcss";
// app/layout.tsx
import "./globals.css";
import "@meridui/react/styles.css";

Without Tailwind, the template adds * { padding: 0; margin: 0 }, which takes the padding off every component, a { color: inherit; text-decoration: none }, which hides Link, and Arial on body. Remove those and keep body { margin: 0 }. The --background and --foreground variables can stay; Merid does not use them.

npx @meridui/cli init makes these changes and asks before writing. It only removes rules that still match the template, so your own edits stay. npx @meridui/cli doctor warns while any of them are left. See Tailwind CSS for mapping Merid's tokens into your Tailwind theme.

Server components

Every interactive component ships its own "use client" directive, so you can import from @meridui/react in a server component and render it directly. Presentational components (Card, Stack, Heading, Text, Badge, Table, …) render on the server with no client JavaScript of their own. Event handlers still need a client boundary — see Server components.

// app/projects/page.tsx — a server component
import { Badge, Card, Heading, Stack, Text } from "@meridui/react";

export default async function Projects() {
  const projects = await db.project.findMany();
  return (
    <Stack gap={4}>
      <Heading level={1} size="h2">Projects</Heading>
      {projects.map((p) => (
        <Card key={p.id} variant="outline">
          <Text tone="ink" weight="medium">{p.name}</Text>
          <Badge tone={p.live ? "success" : "neutral"}>{p.live ? "Live" : "Draft"}</Badge>
        </Card>
      ))}
    </Stack>
  );
}

Components that render an anchor take an as prop, so next/link gives them client-side navigation and prefetching:

import NextLink from "next/link";
import { Breadcrumb, Card, SidebarNav } from "@meridui/react";

<SidebarNav.Item as={NextLink} href="/settings" active={pathname === "/settings"}>Settings</SidebarNav.Item>
<Breadcrumb.Link as={NextLink} href="/projects">Projects</Breadcrumb.Link>
<Card as={NextLink} href={`/projects/${id}`} interactive>…</Card>

Overlay triggers (Dialog.Trigger, Popover.Trigger, DropdownMenu.Trigger, Tooltip) take asChild, so a next/link can also be a tooltip or menu anchor.

Button, Link and Breadcrumb.Link take asChild, which also works in server component files where as={NextLink} cannot be passed:

<Button asChild variant="primary">
  <NextLink href="/new">New project</NextLink>
</Button>

<Link asChild>
  <NextLink href="/pricing">Pricing</NextLink>
</Link>

Pagination with getHref renders plain anchors (full page loads). Pass renderLink to route them through next/link: renderLink={({ page, ...props }) => <NextLink {...props} />}. Or use onPageChange and router.push in an app, and keep getHref for crawlable pagination on public pages.

Fonts

Merid bundles Geist and Geist Mono and loads them through @font-face in its stylesheet, so nothing else is needed. To use next/font instead (self-hosted, preloaded, no layout shift), point the tokens at its variable:

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });

<html lang="en" className={inter.variable}>
/* globals.css */
:root {
  --mrd-font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}

Browsers only download faces that are used, so the bundled Geist files are never requested once the token points elsewhere.

Dark mode without a flash

Set data-theme before first paint with an inline script in <head>, and add suppressHydrationWarning to <html> because the attribute differs from the server render. See Dark mode strategies.