Vue

NibCSS in a Vue 3 app: the Nib plugin, the v-nib directive, useNib, $nib in templates, and SSR.

4 min readv1

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

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

Terminal
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install
npm run dev          # http://localhost:5173

2. Install#

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

3. Add the plugin#

TypeScript
// 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#

TypeScript
// 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

nib="p=16px" — a plain string

the build: the class is compiled into the markup and the CSS into the stylesheet

zero JavaScript, styled on first paint

nib={\p=${pad}px`}` — anything computed

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#

HTML
<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

Nib

the plugin: app.use(Nib, config)

useNib(attr)

a reactive class string (a computed); takes a string, a ref or a getter

useNibRuntime()

the runtime provided by the plugin

v-nib="…"

the directive — applies classes straight to the element

$nib("…")

a class string inside a template, no setup needed

NibStyles / collectNibStyles(rt)

the collected CSS, for a custom SSR setup

NibRuntime

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#

  • :class and nib live together happily. Vue merges a static class with a bound one, so the build writes its class="nib-…" beside your :class and 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 — use v-nib or useNib for anything computed.
  • A recipe name is not a CSS class. nib="btn" works, class="btn" does not — use $nib("btn").

8. Server-side rendering#

TypeScript
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

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.

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; use v-nib or useNib.
  • class="btn" for a recipe — use $nib("btn").
  • Importing @nibcss/styles.css in a component instead of main.ts, so the CSS arrives after the first paint.

Next steps#