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.

3 min readv1

@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#

Terminal
npm i @nibcss/next @nibcss/react @nibcss/runtime

2. Add the preset#

TypeScript
// 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 debug: true

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:

TSX
"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:

TSX
// 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:

TSX
"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#

TSX
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

node_modules/.nib/styles.css

the stylesheet, imported as @nibcss/styles.css

node_modules/.nib/config.mjs, config.json

the resolved config

node_modules/@nibcss/config

a real package re-exporting it, so the import type-checks

node_modules/.nib/boot.mjs

the runtime entry the preset adds to the client bundle

node_modules/.nib/dev.mjs

dev only, debug: true: the overlay and the current warnings

7. Pages Router#

Identical, with the provider in _app.tsx and the stylesheet imported there:

TSX
// 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 dev without --webpack: Turbopack ignores the preset, so nothing is compiled and every attribute falls to the runtime.
  • Passing rewrite to withNib — the preset owns it and will tell you so.
  • Importing @nibcss/styles.css in a client component instead of the layout: it still works, but ships CSS through JavaScript.
  • A dynamic className={expr} next to a literal nib on the same element — the build skips that element on purpose; keep them apart or move the value into useNib.

Next steps#