@nibcss/nuxt

Nuxt module for NibCSS.
329
Install
npm install @nibcss/nuxt
Latest Version:1.1.1
Vue:3
License:MIT
Last Updated:Oct 11, 2026
Links: npm
Maintainer: mvip07

@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
    },
});

shortcuts maps a key to property names. A bundle of declarations belongs in components: center: "d=flex; ali=center; jsc=space-between", used as nib="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

The instant on-demand Atomic CSS engine.

19,637,909 18,983
unocss-preset-vunk

UnoCSS presets for Vunk utilities, including spacing, typography, and flexbox he...

1,259
@hxui/css

Helix User Interface Library - Pure CSS Framework

5,196
vue-styled-components

Visual primitives for the component age. A simple port of styled-components 💅 f...

242,901 1,376

Version History

Version Released Vue License
1.1.1 ^3.0 MIT
1.1.0 ^3.0 MIT
1.0.0 ^3.0 MIT