Svelte & SvelteKit
NibCSS in Svelte 5 and SvelteKit: setupNib, the use:nib action, nibClass, and the one class-attribute rule that trips people up.
@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#
npm create vite@latest my-app -- --template svelte
cd my-app
npm install
npm run dev # http://localhost:51732. Install#
npm i @nibcss/svelte @nibcss/runtime
npm i -D @nibcss/unplugin3. Add the plugin#
// 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#
// 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 | |
|---|---|---|
| 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. A component#
<!-- 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 |
|---|---|
| create and start the page's runtime |
| the runtime |
| the action: |
| a class string, for |
| drop the runtime (tests) |
| 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 ownclass="…"beside it.- Braces inside an attribute are Svelte's interpolation.
nib="p={pad}px"is a template, not a static value — useuse: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 |
|---|---|
| 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#
class={cls} nib="…"on one element — correct, but it silently moves that element to the runtime.- Calling
setupNibinside a component rather than the entry/layout — each instance would start a runtime. nib="p={pad}px"— interpolation; use the action.