Agent-first design consistency for Node-powered web apps.
KDF gives AI agents a JSON source of truth for layout, spacing, typography, and component styling. Agents can build or update UI from that source in server-side JavaScript apps instead of rediscovering design rules across components, pages, and previous sessions.
Think of it like i18n for design: one page maps to one JSON file, and each
rendered element can point back to its exact design key with data-kdf. Users
stay in the approval loop and adjust the source only when intent or product
direction changes.
Styling drifts when class names live only inside .tsx files:
- the same button slowly gets five different variants
- spacing changes page by page
- colors and typography become inconsistent
- each new AI session has to rediscover the design rules
- users spend time correcting visual drift through chat
KDF moves repeatable styling into JSON:
- agents apply tokens before changing UI
- users can review and adjust the source when needed
data-kdfmaps every DOM element back to its exact JSON path- the same design key can be scanned, tested, reviewed, and edited later
KDF works with your existing styling stack: plain CSS, CSS modules, utility CSS, Bootstrap, shadcn, or a custom design system. It is not a component library and not a CSS engine.
KDF has two runtime modes:
-
createDesign(tokens, shared)uses JSON objects imported by the host app. This is the preferred mode for Astro, Next.js, Hono, Cloudflare Workers, and other edge/serverless runtimes because bundlers can see and include the JSON. -
getDesign(page)readskdf/<page>.jsonfrom disk with Nodefs. This is convenient for Node/server apps and local tooling, but it is not the right API for edge runtimes that do not have your local filesystem. -
Works with server-rendered Next.js, Astro, Hono, Cloudflare Workers, or similar runtimes.
-
The included
@kondeio/kdf/pluginexport is the official Next.js integration. -
Next.js plugin target: App Router, Next.js 14+ (
next >=14). -
getDesign()is server-only. Browser/client components should use resolved class strings from a server boundary or use imported JSON withcreateDesign()when the bundler/runtime supports it.
| Framework | Status | How KDF is used |
|---|---|---|
| Next.js | Tested | createDesign() with imported JSON, or getDesign() in Node/server-rendered code plus the optional plugin. |
| Astro | Tested | createDesign() with imported JSON for SSR/edge builds. |
| Hono | Tested | createDesign() with imported JSON in handlers, or getDesign() in Node handlers. |
| Cloudflare Workers | Supported | createDesign() with imported JSON. Do not pass local absolute file paths. |
Install the package:
npm install @kondeio/kdf
pnpm add @kondeio/kdf
bun add @kondeio/kdfBy default, install also scaffolds a starter kdf/ folder when one does not
already exist. Existing files are never overwritten.
To install the dependency without running the initializer:
npm install @kondeio/kdf --ignore-scriptsRun initialization manually later:
npm exec -- kdf initnpm exec -- kdf init uses the local package binary when @kondeio/kdf is already
installed in the project.
Initialize a custom design directory:
KDF_DIR=./designs npm exec -- kdf initkdf/
shared/ <- shared defaults (button, typography, layout, card)
homepage.json <- starter page design
konde-server.css <- critical project overrides
konde.css <- non-critical project overrides
The package repository also keeps additional page JSON examples under
example/sample-pages/. They are reference material only and are not copied by
postinstall or kdf init.
Typical app structure:
my-app/
app/
page.tsx
pricing/page.tsx
components/
hero.tsx
pricing-table.tsx
kdf/
shared/
button.json
card.json
color.json
layout.json
typography.json
homepage.json
konde-server.css
konde.css
next.config.ts
The default design directory is ./kdf.
Use ./designs or ./design through KDF_DIR in any Node/server-side runtime:
KDF_DIR=./designs npm run devimport { getDesign } from "@kondeio/kdf";
const d = getDesign("homepage");In Next.js, the optional plugin sets KDF_DIR for the app:
// next.config.ts
import withKDF from "@kondeio/kdf/plugin";
export default withKDF({ dir: "./designs" })(nextConfig);With that config, KDF reads:
designs/
shared/
homepage.json
konde-server.css
konde.css
Example token:
{
"$layout": ["hero", "features", "footer"],
"hero": {
"wrapper": "mx-auto max-w-6xl px-6 py-20",
"title": "@typography.h1",
"cta": "@button.cta"
}
}Use createDesign() when the app is bundled for Astro, Next.js, Hono, or
Cloudflare Workers:
import { createDesign } from "@kondeio/kdf";
import homepageTokens from "../kdf/homepage.json";
import buttonTokens from "../kdf/shared/button.json";
import typographyTokens from "../kdf/shared/typography.json";
const d = createDesign(homepageTokens, {
button: buttonTokens,
typography: typographyTokens,
});
<h1 data-kdf="hero.title" className={d("hero.title")}>
{t("hero.headline")}
</h1>This makes the JSON a normal build dependency. The bundler includes it in the output instead of KDF trying to read a machine-local path at runtime.
If a runtime or bundler rejects Node built-ins entirely, import the pure entry:
import { createDesign } from "@kondeio/kdf/edge";Use getDesign() in Node/server environments where reading from kdf/*.json at
runtime is intentional:
import { getDesign } from "@kondeio/kdf";
const d = getDesign("homepage");
<h1 data-kdf="hero.title" className={d("hero.title")}>
{t("hero.headline")}
</h1>KDF includes small class composition helpers that work with any UI library:
| Helper | Purpose |
|---|---|
cn() |
Joins conditional classes, drops falsy values, normalizes whitespace, and removes exact duplicates. |
cx() |
Alias of cn(). |
composeClasses() |
Same default composer with a more explicit name. |
dedupeClasses() |
Removes exact duplicates from an existing class string. |
createClassComposer({ merge }) |
Creates a composer with an app-defined merge step. |
The default behavior is intentionally universal. It normalizes whitespace and dedupes exact class names, but does not interpret semantic conflicts such as color, spacing, variant, or framework-specific utility groups.
import { cn, getDesign } from "@kondeio/kdf";
const d = getDesign("homepage");
<button
data-kdf="hero.cta"
className={cn(d("hero.cta"), isActive && d("hero.cta-active"), className)}
>
Start
</button>Remove exact duplicates:
import { dedupeClasses } from "@kondeio/kdf";
dedupeClasses("btn btn btn-primary");
// "btn btn-primary"Add project-specific merge rules:
import { createClassComposer } from "@kondeio/kdf";
const cn = createClassComposer({
merge(className) {
// Example: apply project-specific class rules here.
return className;
},
});getDesign() reads JSON from disk via Node fs, so it runs on the server
only: Next.js Server Components, Astro Node server rendering, Hono Node
handlers, or equivalent Node/server-rendered code. It does not work inside
browser-only code or a Next.js Client Component ("use client"), which has no
filesystem.
For client components, resolve on the server and pass the resulting className string down as a prop:
// Server Component
const d = getDesign("homepage");
return <ClientThing className={d("hero.cta")} />;For edge/serverless deploys, prefer createDesign(importedJson, shared) so the
tokens are bundled as code/data instead of looked up from a runtime path.
Reference shared tokens from shared/:
{ "hero": { "cta": "@button.cta" } }Extend with extra classes:
{ "hero": { "cta": "@button.cta shadow-xl text-lg" } }Multiple refs:
{ "hero": { "login": "@button.base @button.ghost @button.sm" } }Every element using d() should have matching data-kdf:
<div data-kdf="hero.wrapper" className={d("hero.wrapper")}>This makes the rendered UI traceable for agent scanning, visual editing, and Playwright checks.
getDesign() caches design JSON files by default. In development it revalidates
with file mtime/size checks so repeated d() calls do not create disk-read
storms during HMR.
const d = getDesign("homepage", { cache: "auto", maxAgeMs: 250 });Use clearKdfCache() for explicit invalidation in custom tooling.
For values not expressible as reusable classes:
{
"hero": {
"title": {
"className": "text-3xl",
"css": { "--kdf-accent": "oklch(0.546 0.245 262)" }
}
}
}Generated CSS can live in konde.css. The plugin exposes its path via env
(KDF_CLIENT_CSS); wire the <link>/import in your app (e.g. last in
globals.css) — the plugin does not inject it for you.
kdf init- scaffold the starterkdf/folder and generated CSS files.bun example/preview.ts [tailwind|shadcn|bootstrap|pure-css]- run the local KDF example preview. UsePORT=4410to avoid port conflicts andKDF_PREVIEW_OPEN=0for test/CI runs without opening a browser.
Default KDF directory (./kdf):
// next.config.ts
import withKDF from "@kondeio/kdf/plugin";
export default withKDF()(nextConfig);Custom KDF directory (./my-design):
// next.config.ts
import withKDF from "@kondeio/kdf/plugin";
export default withKDF({ dir: "./my-design" })(nextConfig);| Symbol | Purpose | Example |
|---|---|---|
$ |
Component binding | "$": "Button" |
@ |
Shared style reference | "@button.cta" |
$layout |
Page section order | ["hero", "footer"] |
docs/doc.md- KDF concept, architecture, conventions, and operating model.docs/skill.md- agent-facing implementation and review checklist.
See CONTRIBUTING.md for local setup, build/test commands, and PR conventions. Keep the core UI-library agnostic and the resolver server-only.
@kondeio/kdf runs a postinstall script that scaffolds a starter kdf/
folder into your project (never overwrites, no network calls). To skip it:
npm install @kondeio/kdf --ignore-scripts or KDF_SKIP_INIT=1. Full details
and vulnerability reporting in SECURITY.md.
See CHANGELOG.md.
MIT. See LICENSE for the full license text.