i18n is your translation.
KDF is your design.
Both live in JSON.
A JSON-based design coordination layer for server-side web apps and agent-assisted UI. Repeatable decisions — layout, spacing, typography, component styling — live in one source of truth your agents read instead of rediscovering across files, pages, and past sessions.
{ "$layout": ["hero", "footer"], "hero": { "wrapper": "mx-auto max-w-6xl px-6 py-20", "title": "@typography.h1", "cta-primary": "@button.cta" } }
One source of truth keeps every page, component, and agent session consistent.
JSON defines · code renders · data-kdf maps back
Design drifts in
your components.
When class names live only inside .tsx files, every page slowly diverges. It works for a single human editor — but it is fragile the moment agents start touching the UI.
<section className="mx-auto max-w-6xl px-6 py-20"> <h1 className="text-5xl font-semibold tracking-tight"> // …and again, slightly different, on the next page
The same button quietly grows five variants. Spacing changes page by page. Every new agent session has to rediscover the rules from scratch.
- — Agents improvise colors, spacing, typography, and layout.
- — Users correct the result through chat, every time.
- — Changes mean searching through component files.
- — Repeated sessions lose design intent.
→ endless "bigger" · "more blue" · "move left" iteration
Make design explicit.
KDF does for design what i18n does for text: it moves repeatable decisions out of component files and into JSON. The rendered UI still uses normal CSS — what changes is ownership.
{ "hero": { "title": "text-5xl font-semibold tracking-tight" } }
const d = getDesign("homepage"); <h1 data-kdf="hero.title" className={d("hero.title")}>
A library says what.
KDF says which.
A design library gives you a Button. KDF tells you this is homepage.hero.cta-primary, rendered as a Button, styled with this token. It adds the one mapping libraries leave out — element to design decision.
Six symbols.
One grammar.
KDF stores class names, shared references, section order, and CSS custom properties. It never stores business logic, event handlers, data fetching, permissions, or accessibility behavior.
Every element using a token carries a matching path. DOM nodes trace straight back to JSON — for scanners, tests, and agent review.
Resolves a path to its className string. d.css() returns the CSS custom properties object.
Point at a reusable token in shared/. Refs can chain and extend with extra classes.
An array of section keys. Listed sections render in order; missing ones are hidden by the host app.
Metadata for agents and host tooling — which component renders this token, plus variant and size hints.
Values not expressible as reusable classes, applied as inline style variables via d.css().
Shared defaults.
Page overrides.
Design tokens live in two places: reusable defaults in shared/, and per-page composition that overrides only what it needs. Two user-owned CSS files cover first paint and escape hatches.
kdf/ shared/ button.json ← reusable defaults card.json color.json typography.json homepage.json ← page composition konde-server.css ← critical, first paint konde.css ← non-critical tweaks
A reference like @button.cta resolves from the page's shared/, then a parent shared/, then page tokens. Templates override only the parts they need and inherit the rest.
Design resolves on the server; the browser receives finished class strings. Critical CSS lands in the first paint, the rest loads after — no render cost dumped on low-spec clients.
Imported by the app so design variables and no-FOUC overrides arrive in the very first paint — before anything flashes.
/* konde-server.css */ :root { --kdf-primary: #1F8F47; } [data-kdf="hero.slider"] { display: none; }
Loaded after framework and app CSS for tweaks, experiments, and escape hatches — fine-tuning that never needs to block paint.
/* konde.css */ [data-kdf="hero.title"] { letter-spacing: -0.02em; } [data-kdf="hero.wrapper"] { gap: 3rem; }
→ KDF creates both once and never overwrites them. The plugin exposes their paths via env - your app wires the imports; nothing is injected for you.
Switch design
like switching language.
The way i18n switches languages, KDF switches design templates. Same app, same components, same code — point KDF_DIR at another design folder and the whole look changes.
designs/ lander/ shared/ homepage.json newlander/ shared/ homepage.json
// next.config.ts withKDF({ dir: "./designs/lander" })(nextConfig);
Resolve on the server.
Pass strings down.
KDF core reads JSON from disk, so it runs server-side: Next.js Server Components, Astro renders, Hono handlers. Resolve classes there, then hand plain strings to client components.
import { getDesign, cn } from "@kondeio/kdf"; const d = getDesign("homepage"); <button data-kdf="hero.cta" className={cn(d("hero.cta"), isActive)}>Start</button>
→ resolved className string
→ CSS custom-properties object
Join conditional classes, drop falsy values, dedupe — semantic-free by default.
Production caches; dev revalidates by mtime & size.
It sits above
your stack.
STYLING — STORES CLASS NAMES, NOT CSS
KDF is not a CSS engine. Tokens hold whatever class strings your styling system understands. If your framework scans source files, point it at the JSON too: @source "../kdf/**/*.json".
RUNTIME — SERVER-SIDE JAVASCRIPT
Install once.
Init scaffolds the rest.
Installing also scaffolds a starter kdf/ folder when one doesn't exist. Existing files are never overwritten.
import withKDF from "@kondeio/kdf/plugin"; export default withKDF({ dir: "./designs" })(nextConfig);