Svelte & SvelteKit

NibCSS in Svelte 5 and SvelteKit: setupNib, the use:nib action, nibClass, and the one class-attribute rule that trips people up.

3 min readv1

@nibcss/svelte is the smallest of the bindings: one setup call, one action, one helper. Everything else is the attribute you already know.

1. Create a project#

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

2. Install#

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

3. Add the plugin#

JavaScript
// vite.config.js
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { defineConfig } from "vite";
import nib from "@nibcss/unplugin/vite";

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

4. Start the runtime#

JavaScript
// src/main.js
import { mount } from "svelte";
import "@nibcss/styles.css";            // the generated stylesheet
import nibConfig from "@nibcss/config";  // the resolved nibcss.config.ts
import { setupNib } from "@nibcss/svelte";
import App from "./App.svelte";

setupNib(nibConfig);                    // one runtime for the page

export default mount(App, { target: document.getElementById("app") });

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. A component#

HTML
<!-- src/App.svelte -->
<script>
  import { getNib, nib, nibClass } from "@nibcss/svelte";

  const rt = getNib();
  let pad = $state(16);
  let active = $state(false);
</script>

<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">Dashboard</h1>
    <button nib="btn" onclick={() => rt.toggleDarkMode()}>
      {rt.darkMode.isDark() ? "Light" : "Dark"}
    </button>
  </header>

  <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></article>
    <article nib="card-hover; flg=1"><h2 nib="m=0; fts=18px">Users</h2></article>
  </section>

  <!-- a value the runtime resolves, applied by the action -->
  <input type="range" min="4" max="48" bind:value={pad} nib="w=100%; acc=$color.brand" />
  <div use:nib={`p=${pad}px; bd=1px solid $color.border; bdrd=$radius.md`}>
    padding: {pad}px
  </div>

  <!-- or as a class string -->
  <button class={nibClass(active ? "btn; bgc=#15803d" : "btn; bgc=$color.ink-soft")}
          onclick={() => (active = !active)}>
    {active ? "Active" : "Idle"}
  </button>
</main>

6. The API#

Export

What it is

setupNib(config)

create and start the page's runtime

getNib()

the runtime

nib

the action: use:nib={"p=10px"} — re-applied when the value changes

nibClass(attr)

a class string, for class= or a component prop

resetNib()

drop the runtime (tests)

NibRuntime

the class, for a deliberately separate instance

7. Svelte details that matter#

  • class:active={on} is fine — a different attribute, so the build adds its own class="…" beside it.
  • Braces inside an attribute are Svelte's interpolation. nib="p={pad}px" is a template, not a static value — use use:nib={\p=${pad}px`}`.

8. SvelteKit and SSR#

setupNib runs on the server too. Static attributes are already compiled into the HTML SvelteKit sends, so a page with no dynamic values needs no JavaScript at all to look right; the runtime takes over for use:nib and nibClass once it hydrates.

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#

  • class={cls} nib="…" on one element — correct, but it silently moves that element to the runtime.
  • Calling setupNib inside a component rather than the entry/layout — each instance would start a runtime.
  • nib="p={pad}px" — interpolation; use the action.

Next steps#