Vue
NibCSS in a Vue 3 app: the Nib plugin, the v-nib directive, useNib, $nib in templates, and SSR.
@nibcss/vue registers one runtime for the page, the v-nib directive and the $nib template
helper. It works with any bundler Vue is built with — Vite, webpack, Rspack, Rollup. For Nuxt use
@nibcss/nuxt, which wires all of this in one line.
<article nib="card; mxw=420px">
<h2 nib="m=0; fts=18px; ftw=700">Title</h2>
<button nib="btn" @click="save">Save</button>
</article>1. Create a project#
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install
npm run dev # http://localhost:51732. Install#
npm i @nibcss/vue @nibcss/runtime
npm i -D @nibcss/unplugin3. Add the plugin#
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import nib from "@nibcss/unplugin/vite";
export default defineConfig({
plugins: [vue(), nib()],
});nib() needs no options: it writes the stylesheet, publishes the resolved config as
@nibcss/config, and compiles static attributes into class names. Start the app once and
nibcss.config.ts is created at the project root.
4. Install the Vue plugin#
// src/main.ts
import { createApp } from "vue";
import "@nibcss/styles.css"; // the generated stylesheet
import nibConfig from "@nibcss/config"; // the resolved nibcss.config.ts
import { Nib } from "@nibcss/vue";
import App from "./App.vue";
createApp(App).use(Nib, nibConfig).mount("#app");app.use(Nib, config) does three things: starts one runtime for the page with your config,
registers the v-nib directive, and provides the runtime so useNib() and useNibRuntime()
resolve inside components.
Static or dynamic — the one rule to carry#
Where it is resolved | Cost | |
|---|---|---|
| the build: the class is compiled into the markup and the CSS into the stylesheet | zero JavaScript, styled on first paint |
| the browser runtime: resolved and inserted before the next frame | one small runtime, no flash in practice |
You never choose between them by hand. Write the attribute; whatever the build can read, it takes.
5. The four ways to write a value#
<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);
// a reactive class string — takes a string, a ref or a getter
const box = useNib(() => `p=${pad.value}px; bd=1px solid $color.border; bdrd=$radius.md`);
</script>
<template>
<!-- 1. static: compiled at build time -->
<h1 nib="m=0; fts=28px; ftw=800; lts=-.02em">Dashboard</h1>
<!-- 2. useNib: a computed class string -->
<div :class="box">padding: {{ pad }}px</div>
<!-- 3. the directive: re-applied whenever the value changes -->
<div v-nib="`p=${pad}px; bdrd=$radius.md; bgc=#eef2ff; dark:bgc=#1e1b4b`">v-nib</div>
<!-- 4. $nib: a class string inside a template, no setup code -->
<button :class="$nib(active ? 'btn; bgc=#15803d' : 'btn; bgc=$color.ink-soft')" @click="active = !active">
{{ active ? "Active" : "Idle" }}
</button>
<button nib="btn" @click="rt.toggleDarkMode()">
{{ rt.darkMode.isDark() ? "Light" : "Dark" }}
</button>
</template>6. The API#
Export | What it is |
|---|---|
| the plugin: |
| a reactive class string (a |
| the runtime provided by the plugin |
| the directive — applies classes straight to the element |
| a class string inside a template, no setup needed |
| the collected CSS, for a custom SSR setup |
| the class, for a deliberately separate instance |
$nib is declared on ComponentCustomProperties, so it type-checks in a template with no extra
setup.
7. Vue details that matter#
:classandniblive together happily. Vue merges a staticclasswith a bound one, so the build writes itsclass="nib-…"beside your:classand both apply. (This is where Vue differs from React and Svelte, which refuse the pair.)- Interpolation makes a value dynamic.
nib="p={{ pad }}px"is not a static attribute — usev-niboruseNibfor anything computed. - A recipe name is not a CSS class.
nib="btn"works,class="btn"does not — use$nib("btn").
8. Server-side rendering#
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";
import { NibRuntime } from "@nibcss/runtime";
import { Nib, collectNibStyles } from "@nibcss/vue";
import config from "@nibcss/config";
const rt = new NibRuntime({ watch: false, ...config });
const app = createSSRApp(App).use(Nib, { runtime: rt });
const body = await renderToString(app);
const html = `<!doctype html><html><head>
<style id="nib">${collectNibStyles(rt)}</style>
</head><body><div id="app">${body}</div></body></html>`;What the build generates#
Path | What it is |
|---|---|
| the stylesheet, imported as |
| the resolved config, as a module and as data |
| a real package re-exporting it, so |
| webpack/Rspack: the runtime entry appended to the client bundle |
| dev only, with |
Everything lives inside node_modules, so there is nothing to add to .gitignore.
Common pitfalls#
- Skipping
app.use(Nib, …)— the hooks have nothing to resolve, and SSR fails on the unregistered directive. nib="p={{ pad }}px"— interpolation inside a static attribute; usev-niboruseNib.class="btn"for a recipe — use$nib("btn").- Importing
@nibcss/styles.cssin a component instead ofmain.ts, so the CSS arrives after the first paint.