Vite, webpack, Rspack, Rollup, esbuild

@nibcss/unplugin is one plugin for five bundlers: the same options, the same generated files, the same dev-server behaviour.

3 min readv1

@nibcss/unplugin is the build half of NibCSS. Whatever the framework, the plugin does the same four things: read nibcss.config.ts, scan your sources, write the stylesheet, and compile static attributes into class names.

1. Install#

Terminal
npm i @nibcss/runtime
npm i -D @nibcss/unplugin

2. Add it#

TypeScript
import { defineConfig } from "vite";
import nib from "@nibcss/unplugin/vite";

export default defineConfig({ plugins: [nib()] });

Then import the two things the plugin publishes:

TypeScript
import "@nibcss/styles.css";            // the generated stylesheet
import nibConfig from "@nibcss/config";  // the resolved nibcss.config.ts

With webpack and Rspack the plugin also appends the runtime to your client entry, so a project with no provider still works. With Vite, hand the config to your framework's bindings — <NibProvider {...nibConfig}>, app.use(Nib, nibConfig), setupNib(nibConfig).

3. Options#

Everything about styling lives in nibcss.config.ts. These are the build-only decisions:

Option

Default

What it does

rewrite

"keep" in dev, true in a build

compile static attributes into classes. true replaces the attribute; "keep" adds the class and leaves it, so the runtime can re-resolve it when the config changes; false leaves the markup alone

runtime

true

webpack/Rspack: append the runtime to the client entry

devtools

true

mount the diagnostics overlay in dev (needs debug: true)

config

auto-detected

path to the config file

include / exclude

project sources

extra globs to scan, or skip

override

—

config fields that win over the file

scaffold

true

false stops the plugin creating the config file

client

auto-detected

tell the plugin which compiler is the browser bundle

TypeScript
nib({
  include: ["src/**/*.{ts,tsx,html}", "docs/**/*.md"],
  exclude: ["**/*.stories.tsx"],
  override: { debug: process.env.CI !== "true" },
})

What the build 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, as a module and as data

node_modules/@nibcss/config

a real package re-exporting it, so import config from "@nibcss/config" type-checks with no paths entry

node_modules/.nib/boot.mjs

webpack/Rspack: the runtime entry appended to the client bundle

node_modules/.nib/dev.mjs

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

Everything lives inside node_modules, so there is nothing to add to .gitignore.

Dev-server behaviour#

  • A config edit is applied to the page you have open — no reload, no restart. The build pushes the new config and the new stylesheet over the dev channel, and the page swaps the stylesheet and re-resolves its elements. Rules the change removes go away too.
  • A source edit is a normal hot update.
  • Diagnostics are served at /__nib/warnings and pushed to the overlay as you type.

Every generated file opens with /* eslint-disable */ and an @generated marker, so linters and reviewers leave them alone.

Common pitfalls#

  • Expecting Turbopack support — it has no plugin API yet; run Next with --webpack.
  • Setting rewrite: false and wondering why production ships attributes instead of classes.
  • A monorepo where the plugin picks the wrong config: point config at the right file.
  • Scanning node_modules through a wide include glob — it is skipped on purpose; keep it that way.

Next steps#