Getting started
Installation
Plinth is source code, not a package. You set up Next.js and Tailwind CSS 4, copy the design tokens, then copy the component files you need.
On this page
How installation works
There is no npm package and no CLI. Everything lives in the files you downloaded: app/globals.css holds the tokens, and components/ui holds one file per component. You copy them into your project and they become your code, so you can change anything without waiting for an update.
Requirements
- Node.js 20.9 or later, and a package manager (pnpm, npm or yarn).
- React 19 and Next.js 16 with the App Router. Other React 19 setups work too; only the font and boot-script steps are Next.js specific.
- Tailwind CSS 4. Plinth uses
@themeand@custom-variant, which Tailwind 3 does not support. - TypeScript 5 and lucide-react for icons. There are no other dependencies.
Set up a project
Create a Next.js app
Create an app with TypeScript, Tailwind CSS and the App Router, then add Lucide.
Terminalpnpm create next-app@latest ledgerly --typescript --tailwind --eslint --app --no-src-dir --import-alias "@/*"cd ledgerlypnpm add lucide-reactAdd Tailwind CSS 4
create-next-appalready sets up Tailwind CSS 4. Adding Plinth to an existing app without it? Install Tailwind and its PostCSS plugin:pnpm add -D tailwindcss @tailwindcss/postcss postcss// postcss.config.mjsconst config = { plugins: { "@tailwindcss/postcss": {} },}; export default config;Copy the tokens
Replace your
app/globals.csswith the one from the kit. It imports Tailwind, defines the light and dark palettes, the six accent presets, the radius scale and the type utilities. Components only use these tokens, so this file is required.appglobals.css— tokens, presets, utilitieslayout.tsxCopy the components you need
Copy
components/ui/cn.tsfirst: it is the only shared helper. Then copy the component files you want. Each file lists its imports at the top; when it imports a sibling (for example./badge), copy that file too.componentsuicn.ts— class name helperbutton.tsxbadge.tsxcard.tsxdialog.tsxcharts.tsx— line, bar, donut, sparkline…— one file per componentThe helper is three lines. It joins class names and skips falsy values; it does not merge conflicting Tailwind classes, so component props are designed to never need overrides.
components/ui/cn.ts// components/ui/cn.tsexport function cn(...classes: Array<string | false | null | undefined>) { return classes.filter(Boolean).join(" ");}Fonts and the theme boot script
Plinth uses Instrument Sans and DM Mono from Google Fonts through
next/font. The variable names must match--font-instrumentand--font-dmmonoin the tokens. The inline script applies the saved theme, accent and radius before the page paints.app/layout.tsx// app/layout.tsximport { DM_Mono, Instrument_Sans } from "next/font/google";import "./globals.css"; const instrument = Instrument_Sans({ subsets: ["latin"], weight: ["400", "500", "600", "700"], variable: "--font-instrument" });const dmMono = DM_Mono({ subsets: ["latin"], weight: ["400", "500"], variable: "--font-dmmono" }); // Applies the saved theme, accent and radius before first paint (no flash).const themeBoot = `(function(){try{var s=localStorage,e=document.documentElement,t=s.getItem("theme"),d=t==="dark"||(t==="system"&&matchMedia("(prefers-color-scheme: dark)").matches);e.classList.toggle("dark",d);var a=s.getItem("accent");if(a)e.setAttribute("data-accent",a);var r=s.getItem("radius");if(r)e.setAttribute("data-radius",r)}catch(x){}})()`; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning className={`${instrument.variable} ${dmMono.variable}`}> <head> <script dangerouslySetInnerHTML={{ __html: themeBoot }} /> </head> <body className="bg-canvas text-fg antialiased">{children}</body> </html> );}Use a component
Import from
@/components/uiand compose. Everything follows the current accent, radius and theme.app/page.tsx// app/page.tsximport { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";import { Card, CardBody, CardHeader } from "@/components/ui/card"; export default function Page() { return ( <main className="mx-auto max-w-md p-10"> <Card> <CardHeader title="Fernhill Studio" description="Invoice INV-0412" action={<Badge tone="success" dot>Paid</Badge>} /> <CardBody> <Button variant="primary">Download PDF</Button> </CardBody> </Card> </main> );}
Using the HTML edition
Not using React? The html folder in your download has every page as plain HTML with one compiled stylesheet and a small script for the interactive parts: theme toggle, accent and radius presets, tabs, menus, dialogs, copy buttons and search. Open index.html in a browser or upload the folder to any static host.
- Copy the markup of any component straight from the HTML pages; the classes are the same as in the React edition.
- Change colors by editing the variables at the top of the stylesheet. Rebuild it with the Tailwind CLI only when you add new classes (see
html/README.md). - Presets work with data attributes: any element with
data-accent-set="violet"ordata-radius-set="large"applies that preset when clicked.
Troubleshooting
Components render without styles
Check that app/globals.css is imported in your root layout and starts with @import "tailwindcss";. Tailwind 4 detects class names in your project automatically. If your components live outside the app folder (for example in a monorepo package), point Tailwind at them:
/* app/globals.css: only if your components live outside the project root */@import "tailwindcss";@source "../packages/ui";Dark mode does not switch
Plinth switches on the dark class on <html>, not on the system setting. Keep the @custom-variant dark line from the tokens file and toggle the class (the ThemeToggle component does this and saves the choice).
The page flashes the wrong theme on load
The boot script must run in <head> before the body renders. Keep it inline (not in a separate file loaded with defer) and keep suppressHydrationWarning on <html>, because the script changes its attributes before React hydrates.
Module not found: @/components/ui/cn
The components import with the @/ alias. Add "paths": { "@/*": ["./*"] } to compilerOptions in tsconfig.json, or change the imports to relative paths.
Fonts fall back to the system font
The variable names passed to next/font must be --font-instrument and --font-dmmono, and the classes they return must be on <html>. Using other fonts? Change --font-sans and --font-mono in the tokens file.
Next, choose your accent and radius in the theme customizer or read how theming works.