@nibcss/nuxt
@nibcss/nuxt
NibCSS for Nuxt — one module line, and your styling is an attribute.
<article nib="card; mxw=420px">
<h2 nib="m=0; fts=18px; ftw=700">Title</h2>
<button nib="btn" @click="save">Save</button>
</article>
The server-rendered HTML carries the class names, in development and in a build, so the first paint is styled — SSR included.
1. Create a Nuxt project
npx nuxi@latest init my-app
cd my-app
npm install
npm run dev # http://localhost:3000
2. Install NibCSS
npm i @nibcss/vue @nibcss/runtime
npm i -D @nibcss/nuxt
@nibcss/core, @nibcss/unplugin and @nibcss/devtools come along as
dependencies.
3. Add the module
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nibcss/nuxt"],
});
That is the whole setup. The module wires four things, all reading the same
nibcss.config.ts:
| Piece | What it does |
|---|---|
| the Vite plugin | scans your components, writes the stylesheet, publishes @nibcss/config, compiles static attributes into classes |
| a Vue plugin | one runtime for the page, v-nib, and the runtime for useNib / useNibRuntime — on the server too |
| a server plugin (dev) | re-reads the config per render, so the server-rendered half never lags behind an edit, and sets the dark-mode class before the first paint |
| two client plugins (dev) | apply a config change to the open page, and mount the diagnostics overlay |
The stylesheet is added to nuxt.options.css for you. There is nothing to
import and no provider to write.
Options go under a nib key when you need them:
| Option | Default | What it does |
|---|---|---|
runtime |
true |
install the Vue plugin automatically |
devtools |
true |
mount the overlay in dev (needs debug: true) |
config |
beside nuxt.config.ts |
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 module creating the config file |
rewrite is not an option here: the module owns it — the attribute is kept
beside the class in development, and replaced in a build.
Then start the app once — nibcss.config.ts is created for you beside
nuxt.config.ts, not inside app/.
4. A mini project
<!-- app/app.vue -->
<script setup lang="ts">
import { ref } from "vue";
import { useNib, useNibRuntime } from "@nibcss/vue";
const rt = useNibRuntime();
const pad = ref(16);
const active = ref(false);
const box = useNib(() => `p=${pad.value}px; bd=1px solid $color.border; bdrd=$radius.md`);
</script>
<template>
<main nib="mxw=720px; mi=auto; p=$space.xl $space.md; d=flex; flxd=column; g=$space.lg">
<header nib="d=flex; ali=center; jsc=space-between; g=$space.md">
<h1 nib="m=0; fts=28px; ftw=800; lts=-.02em; cl=$color.ink; dark:cl=#f8fafc">
Dashboard
</h1>
<button nib="btn" @click="rt.toggleDarkMode()">
{{ rt.darkMode.isDark() ? "Light" : "Dark" }}
</button>
</header>
<!-- a recipe, and a breakpoint: column on phones, row from md up -->
<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>
<article nib="card-hover; flg=1">
<h2 nib="m=0; fts=18px">Users</h2>
<strong nib="fts=32px; ftw=800">1,204</strong>
</article>
</section>
<!-- values the runtime resolves -->
<input type="range" min="4" max="48" v-model.number="pad" nib="w=100%; acc=$color.brand" />
<div :class="box">padding: {{ pad }}px</div>
<div v-nib="`p=${pad}px; bdrd=$radius.md; bgc=#eef2ff; dark:bgc=#1e1b4b`">v-nib</div>
<button :class="$nib(active ? 'btn; bgc=#15803d' : 'btn; bgc=$color.ink-soft')"
@click="active = !active">
{{ active ? "Active" : "Idle" }}
</button>
</main>
</template>
@keyframes and page-level rules (the body background, a font stack) stay in
CSS — they are global rules rather than utilities. A <style> block in
app.vue is the usual home for them.
5. Editing the config while the server runs
Save nibcss.config.ts and the page you have open updates by itself — no manual
refresh, no reload, no restart. prefix, darkMode, debug, pseudos,
preflight, strict, tokens, recipes and shorthands are all picked up in about
a second, and rules the change removes go away too.
It works by being told rather than by reloading: the build pushes the new config
and the new stylesheet over the dev channel, and the open page swaps the
stylesheet and re-resolves its elements. The one exception is language: the
overlay's own labels are baked in when its module loads, so those follow on the
next reload.
6. Dark mode arrives dark
With darkMode: "class" the module inlines a few bytes at the top of <head>
that set html.dark from the stored choice, or the OS preference when there is
none, before the first paint. Without it the runtime adds the class after
hydration, which in a dark app is a white flash on every load. Nothing to
configure.
7. What the module generates
| Path | What it is |
|---|---|
node_modules/.nib/styles.css |
the stylesheet, added to nuxt.options.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 the import type-checks |
.nuxt/nib-vue.mjs, nib-vue-server.mjs, nib-hot.client.mjs, nib-devtools.client.mjs |
the generated plugins (virtual in dev — .nuxt/types/build.d.ts lists them) |
Everything is inside node_modules or .nuxt, so there is nothing to add to
.gitignore.
nibcss.config.ts
One file, read by three consumers — the build plugin, the CLI, and the browser runtime — so a change lands everywhere at once. It is created for you on the first run, with every option documented inside. Every field is optional.
import { defineConfig } from "@nibcss/core";
export default defineConfig({
// ── Dark mode ──────────────────────────────────────────────────────
// "class" → .dark .nib-x { … } toggle with runtime.toggleDarkMode()
// "media" → @media (prefers-color-scheme: dark) { … }
// false → every `dark:` rule is dropped
darkMode: "class",
// ── Diagnostics ────────────────────────────────────────────────────
// Report unknown utilities, unknown tokens and invalid values, with the
// file and line. With a bundler plugin this also mounts the browser
// overlay. Turning it off and on again needs no restart.
debug: true,
// Language for every message, terminal and overlay alike.
// Built in: uz, en, ru, tr, es, de, zh. Default: "uz".
language: "en",
// Add or re-word messages — your own language, or your team's wording.
// A key you leave out falls back to English.
messages: {
fr: {
"invalid-value": "« {value} » est invalide pour {property}",
accepts: "valeurs acceptées : {examples}",
},
},
// true → an invalid declaration is dropped instead of emitted (CI)
// false → it is written out and the browser ignores it (iterating)
strict: false,
// ── Base reset ─────────────────────────────────────────────────────
// true → the built-in reset, false → none, string → your own CSS.
// Emitted inside `@layer nib-preflight`, so your stylesheet always wins
// over it and utilities always win over both.
preflight: true,
// ── Class names ────────────────────────────────────────────────────
// Prefix for every generated class. A class name is a hash of the rule,
// so the same rule always produces the same name — on every machine.
prefix: "nib-",
// Allow `hover:`, `focus:`, `before:`, `nth-child:` … variants.
pseudos: true,
// ── Breakpoints — merged over the eight built-ins ───────────────────
// Built in: xs 480, sm 576, md 768, lg 992, xl 1024, 2xl 1200,
// 3xl 1440, 4xl 1536.
breakpoints: {
tablet: { min: 768, max: 1023 }, // ranges are allowed
"3xl": { min: 1600 }, // override one
},
// ── Design tokens ──────────────────────────────────────────────────
// Referenced in any value as `$group.key` or `$group-key`. The groups
// are arbitrary; these are a starting point, not a schema.
tokens: {
color: {
brand: "#4f46e5",
"brand-hover": "#4338ca",
ink: "#0f172a",
"ink-soft": "#64748b",
surface: "#ffffff",
"surface-dark": "#0f172a",
border: "#e2e8f0",
"border-dark": "#1e293b",
},
space: { xs: "4px", sm: "8px", md: "16px", lg: "24px", xl: "40px" },
radius: { sm: "8px", md: "12px", lg: "20px", pill: "999px" },
// any CSS value works, gradients and shadows included
gradient: { brand: "linear-gradient(135deg, #4f46e5, #ec4899)" },
},
// ── Component recipes ──────────────────────────────────────────────
// A named set of utilities, used as a bare word (no `=`):
// nib="btn" the whole recipe
// nib="btn; fts=16px" the recipe plus an override
// nib="md:card" the prefix flows into every rule in it
// A recipe may extend another by naming it first, as "card-hover" does.
components: {
btn:
"d=inline-flex; ali=center; jsc=center; g=8px; p=10px 18px; " +
"bdrd=$radius.sm; bd=none; cur=pointer; ftw=600; fts=14px; " +
"bgc=$color.brand; cl=#fff; ts=background-color .15s; " +
"hover:bgc=$color.brand-hover",
card:
"d=flex; flxd=column; g=$space.md; p=$space.lg; bdrd=$radius.lg; " +
"bgc=$color.surface; bd=1px solid $color.border; " +
"dark:bgc=$color.surface-dark; dark:bd=1px solid $color.border-dark",
"card-hover": "card; ts=transform .2s; hover:tf=translateY(-4px)",
},
// ── Custom shorthands — merged over the ~290 built-ins ──────────────
// Map your own key to one or more CSS *property names*:
shortcuts: {
size: "width height", // nib="size=40px" → both
tap: "-webkit-tap-highlight-color", // shorten a vendor property
},
});
shortcutsmaps a key to property names. A bundle of declarations belongs incomponents:center: "d=flex; ali=center; jsc=space-between", used asnib="center".
Where the file lives
Beside package.json, at the project root — the same place as
vite.config.ts, next.config.ts or nuxt.config.ts. nibcss.config.js,
.mjs, .cjs and .mts work too, and nib.config.* is accepted as a second
name.
The attribute syntax
One attribute, semicolon-separated declarations:
<div nib="d=flex; ali=center; g=8px; p=$space.md; hover:bgc=#eef2ff">
A declaration
shorthand=value. The shorthand is the CSS property with the vowels removed —
d for display, bgc for background-color, fts for font-size,
bdrd for border-radius. About 290 are built in, and the full property name
always works too: background-color=red is the same as bgc=red.
| Written | CSS |
|---|---|
fts=14px |
font-size: 14px |
p=10px 18px |
padding: 10px 18px |
bd=1px solid #eee |
border: 1px solid #eee |
bgi=linear-gradient(90deg, red, blue) |
background-image: … |
ts=transform .25s ease |
transition: … |
cl=red!important |
color: red !important |
Values are passed through as written, so calc(), var(), clamp(),
rgb(… / .5), conic-gradient(…) and custom properties all work. Commas and
spaces inside a value need no escaping; a literal semicolon does: write
\;.
<span nib="after:cnt=' \; done'">
Tokens
$group.key or $group-key, resolved from tokens in the config:
<div nib="p=$space.lg; bgc=$color.brand; bd=1px solid $color.border">
An unknown token is reported with the nearest match rather than silently producing nothing.
Recipes
A bare word with no = is a name from components:
<button nib="btn">Save</button>
<button nib="btn; fts=16px; px=24px">Save</button> <!-- plus overrides -->
<article nib="card-hover; flg=1">…</article>
A recipe name is not a real CSS class: nib="btn" works, class="btn" does
not. Ask the runtime for the string when you need one.
Variants
Prefixes stack, separated by : — breakpoint, then state, then the
declaration:
| Prefix | Example | Meaning |
|---|---|---|
| breakpoint | md:d=flex |
from 768px up |
| custom breakpoint | tablet:d=grid |
whatever you defined |
| pseudo-class | hover:bgc=#333, focus-visible:otl=2px solid |
state |
| form state | required:bdl=4px solid #f59e0b, disabled:opc=.5, user-invalid:bdc=#b91c1c |
|
| structural | first-child:ftw=700, nth-child(2):bgc=#eee, empty:h=20px |
|
| pseudo-element | before:cnt='▸', after:cl=#15803d, first-letter:fts=34px, marker:cl=red, selection:bgc=#4f46e5 |
|
| vendor | -webkit-scrollbar:w=10px, -webkit-scrollbar-thumb:bgc=#888 |
|
| dark | dark:bgc=#0b0f19 |
per darkMode |
| combined | md:hover:bgc=#333, dark:hover:bgc=#1e1b4b |
A variant in front of a recipe applies to the whole recipe: md:btn.
Static or dynamic
A plain string attribute is static: the build resolves it, writes the CSS, and compiles the class name into the markup, so the first paint is styled and no JavaScript is needed for it.
Anything the build cannot read — a template literal, an expression, a value your code assembles — is dynamic: the browser runtime resolves it, adds the rule before the next frame, and keeps it in its own stylesheet.
<!-- static: resolved at build time -->
<div nib="p=16px">
<!-- dynamic: resolved in the browser -->
<div nib={`p=${pad}px`}>
License
MIT
Related Packages
UnoCSS presets for Vunk utilities, including spacing, typography, and flexbox he...
Visual primitives for the component age. A simple port of styled-components 💅 f...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.1.1 | ^3.0 | MIT | |
| 1.1.0 | ^3.0 | MIT | |
| 1.0.0 | ^3.0 | MIT |