Installation
Add prfct to a new or existing Next.js project in a few minutes — with the shadcn CLI or by hand.
prfct is distributed as open code: components are copied into your project, where you own them. The shadcn CLI handles the copying and the dependencies; prfct's registry provides the theme and the components.
Requirements
- Next.js 15 or later with the App Router (Vite, TanStack Start and React Router work too)
- React 19
- Tailwind CSS v4
- Node.js 20.9 or later
Create a project
Skip this step if you already have a project with shadcn set up. Otherwise, this creates a Next.js app with shadcn configured for Base UI, the primitive library prfct's components are built on:
$ pnpm dlx shadcn@latest init --template next --base baseIn an existing Next.js project, run shadcn@latest init --base base instead.
Install the prfct theme
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/prfct.jsonThis copies the tokens, base styles, motion and focus utilities and the font faces into styles/prfct/, adds the token-aware cn() to lib/utils.ts and a ThemeProvider, and installs the dependencies.
Import the styles
Replace your global stylesheet with the four imports below. prfct's tokens replace the :root, .dark and @theme inline blocks that shadcn init generated — if you keep them, they override prfct's values.
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "../styles/prfct/index.css";
@custom-variant dark (&:where(.dark, .dark *));Add the fonts
prfct sets its type in Inter 4.1, self-hosted so that every OpenType feature survives subsetting. Download the two files into public/fonts:
curl --create-dirs -o public/fonts/inter-4.1-variable.woff2 https://www.prfct.dev/fonts/inter-4.1-variable.woff2
curl -o public/fonts/inter-4.1-variable-italic.woff2 https://www.prfct.dev/fonts/inter-4.1-variable-italic.woff2Then update the root layout: remove the sans font that next/font loads (prfct's stylesheet defines --font-sans), load Geist Mono as --font-geist-mono, preload Inter and wrap the app in the ThemeProvider.
import { Geist_Mono } from "next/font/google"
import ReactDOM from "react-dom"
import { ThemeProvider } from "@/components/theme-provider"
import "./globals.css"
const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" })
export default function RootLayout({ children }: { children: React.ReactNode }) {
ReactDOM.preload("/fonts/inter-4.1-variable.woff2", {
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
})
return (
<html lang="en" className={geistMono.variable} suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}Add components
Add components one at a time, as you need them. Each one brings the components it builds on:
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/button.jsonOr register prfct as a namespace once, then use short names:
{
"registries": {
"@prfct": "https://www.prfct.dev/r/{name}.json"
}
}$ pnpm dlx shadcn@latest add @prfct/button @prfct/dialog @prfct/fieldUse them
import { Button } from "@/components/ui/button"
export default function Page() {
return <Button>Get started</Button>
}shadcn init --template next --base base: the theme and components install, type-check and build with next build.Manual installation
Prefer to see every file? Each component page has a Manual tab with its source and dependencies. The foundation is small:
- Styles. Copy the five files into
styles/prfct/, namingstyles/prfct.cssindex.css. It importsfonts.css,tokens.css,base.cssandutilities.cssfrom the same folder. Importstyles/prfct/index.cssfrom your global stylesheet after Tailwind, as above. - Utilities.
lib/utils.tsexports the token-awarecn(). Install its one dependency withpnpm add cn. - Fonts. The two Inter files in
public/fonts, and Geist Mono throughnext/fontas--font-geist-mono. Without it, code falls back to the platform's monospace. - Packages.
@base-ui/react,class-variance-authority,lucide-react,next-themes,shadcn(for its Tailwind variants) andtw-animate-css.
Dark mode
The ThemeProvider follows the system preference and stores the person's choice. Components never use dark: — they read tokens that switch with the .dark class, which also works on any subtree. See Theming.
Updating
Because the code is yours, updates are opt-in. Preview what changed upstream before accepting it:
$ pnpm dlx shadcn@latest add @prfct/button --diff button.tsx