Next.js
One-line setup for Next.js with @nibcss/next: build-time classes in server components, the runtime for dynamic values, dark mode with no flash.
@nibcss/next is a preset: it owns every build decision that is not a styling choice, so a Next app
needs exactly one line of configuration.
1. Install#
npm i @nibcss/next @nibcss/react @nibcss/runtime2. Add the preset#
// next.config.ts
import type { NextConfig } from "next";
import { withNib } from "@nibcss/next";
const nextConfig: NextConfig = {
/* your options */
};
export default withNib(nextConfig);That is the whole setup:
Development | Build | |
|---|---|---|
Static attributes | compiled to classes, attribute kept | compiled, attribute removed |
Runtime in the bundle | yes | yes, for dynamic values |
Diagnostics overlay | yes, when | never |
Options go to withNib(nextConfig, { include, exclude, config, devtools }) when you need them.
rewrite is not one of them: the preset owns it, and says so once if you pass it.
3. Wire the provider#
The stylesheet goes in the root layout — a server component, so importing it there puts no JavaScript in the browser — and the provider in a client component:
"use client";
import { NibProvider } from "@nibcss/react";
import nibConfig from "@nibcss/config";
export function NibClientProvider({ children }: { children: React.ReactNode }) {
return <NibProvider {...nibConfig}>{children}</NibProvider>;
}4. Server components need nothing#
A static nib attribute in a server component is compiled at build time — the HTML Next sends already
carries the class, and no client JavaScript is involved:
// app/page.tsx — a server component
export default function Page() {
return (
<main nib="mxw=720px; mi=auto; p=$space.xl $space.md; d=flex; flxd=column; g=$space.lg">
<h1 nib="m=0; fts=28px; ftw=800; lts=-.02em">Dashboard</h1>
<section nib="d=flex; flxd=column; g=$space.md; md:flxd=row">
<article nib="card-hover; flg=1">
<h2 nib="m=0; fts=18px">Revenue</h2>
<strong nib="fts=32px; ftw=800">$18,402</strong>
</article>
</section>
</main>
);
}Anything computed belongs in a client component, with the hooks:
"use client";
import { useState } from "react";
import { useNib, useNibRuntime } from "@nibcss/react";
export function PaddingDemo() {
const runtime = useNibRuntime();
const [pad, setPad] = useState(16);
const box = useNib(`p=${pad}px; bd=1px solid $color.border; bdrd=$radius.md`);
return (
<>
<input type="range" min="4" max="48" value={pad} onChange={(e) => setPad(+e.target.value)} nib="w=100%" />
<div className={box}>padding: {pad}px</div>
<button nib="btn" onClick={() => runtime.toggleDarkMode()}>Theme</button>
</>
);
}5. Dark mode without a flash#
import { darkModeBootScript } from "@nibcss/runtime";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: darkModeBootScript() }} />
</head>
<body>{children}</body>
</html>
);
}darkMode: "class" means a class on <html>, which React can only add after hydration — a white
flash on every load for a dark app. The boot script reads the stored choice, or the OS preference when
there is none (the same rule the runtime uses), and sets the class before the first paint.
6. What the preset generates#
Path | What it is |
|---|---|
| the stylesheet, imported as |
| the resolved config |
| a real package re-exporting it, so the import type-checks |
| the runtime entry the preset adds to the client bundle |
| dev only, |
7. Pages Router#
Identical, with the provider in _app.tsx and the stylesheet imported there:
// pages/_app.tsx
import "@nibcss/styles.css";
import type { AppProps } from "next/app";
import { NibProvider } from "@nibcss/react";
import nibConfig from "@nibcss/config";
export default function App({ Component, pageProps }: AppProps) {
return (
<NibProvider {...nibConfig}>
<Component {...pageProps} />
</NibProvider>
);
}Common pitfalls#
- Running
next devwithout--webpack: Turbopack ignores the preset, so nothing is compiled and every attribute falls to the runtime. - Passing
rewritetowithNib— the preset owns it and will tell you so. - Importing
@nibcss/styles.cssin a client component instead of the layout: it still works, but ships CSS through JavaScript. - A dynamic
className={expr}next to a literalnibon the same element — the build skips that element on purpose; keep them apart or move the value intouseNib.