Konde Design Framework

i18nが翻訳なら、
KDFはデザインだ。
どっちもJSONにある。

サーバーサイドのWebアプリやAIエージェントを活用したUI開発のための、JSONベースのデザイン調整レイヤーです。レイアウトや余白、タイポグラフィ、コンポーネントのスタイルなど、繰り返し使われるデザインのルールを一つのファイル(SSOT)にまとめます。エージェントは過去の履歴や色々なファイルを漁る必要がなくなり、このJSONを読むだけでデザインを把握できるようになります。

仕組みを見てみる
kdf / homepage.json → レンダリング後
kdf/homepage.json
{
  "$layout": ["hero", "footer"],
  "hero": {
    "wrapper": "mx-auto max-w-6xl px-6 py-20",
    "title":   "@typography.h1",
    "cta-primary": "@button.cta"
  }
}
data-kdf="hero.title"
AIと一緒に開発・リリース。
data-kdf="hero.body"

一つのソース・オブ・トゥルース(SSOT)が、すべてのページ、コンポーネント、エージェントのセッションに一貫性をもたらします。

data-kdf="hero.cta-primary"
はじめる →

JSONで定義 · コードで描画 · data-kdf が元と紐づく

ライセンス
MIT
ランタイム
Node Filesystem
依存関係
ランタイム依存なし
動作確認済
Next · Astro · Hono
01 — 抱えている課題

コンポーネントの
デザインがバラバラになる。

クラス名が .tsx ファイルの中にしかないと、ページごとに少しずつデザインがズレていきます。一人の人間が編集しているうちはいいですが、AIエージェントにUIを触らせた途端に破綻しやすくなります。

kdfなしの場合 · ファイル中に散乱
<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

同じボタンなのにいつの間にか5つもバリエーションができていたり、ページごとに余白が違っていたり。新しいエージェントのセッションを立ち上げるたびに、デザインのルールをゼロから教え直すハメになります。

結果こうなる
  • エージェントが色や余白、フォント、レイアウトを適当にでっち上げる。
  • その度に人間がチャットで修正を指示しないといけない。
  • 変更するたびにコンポーネントファイルを探し回ることになる。
  • セッションを繰り返すうちにデザインの意図が失われていく。

→ 終わりのない "もっと大きく" · "もうちょい青く" · "左に寄せて" 修正ループ

02 — 解決策

デザインを明文化する。

i18nがテキストをコードから分離したように、KDFはデザインの決定事項をコンポーネントからJSONへと移します。レンダリングされたUIは普通のCSSを使いますが、「誰がデザインのルールを持っているか」が変わるんです。

1 — jsonで定義する
{
  "hero": {
    "title": "text-5xl font-semibold tracking-tight"
  }
}
2 — コードで描画する
const d = getDesign("homepage");

<h1 data-kdf="hero.title" className={d("hero.title")}>
JSONが定義する
1つのファイルがデザインの方向性を握っています。細かくコントロールしたい時は人間が直接ここを編集します。
コードが描画する
コンポーネントはトークンを読み取り、承認されたデザインを通常のCSSでレンダリングします。裏で魔法みたいなことはしていません。
エージェントが実装する
エージェントは推測でコードを書くのではなく、JSONからUIを構築します。勝手に余白や色、フォント、セクションの順番を作り出すことはもうありません。
人間がJSONを調整する
セッションのたびにチャットで同じ見た目の修正を指示するのではなく、大元のソースを一度変更するだけで済みます。
03 — コンポーネントライブラリじゃない

ライブラリは「何であるか」を言う。
KDFは「どれか」を指定する。.

デザインライブラリはボタンを提供してくれます。一方でKDFは、これが homepage.hero.cta-primary であり、ボタンとしてレンダリングされ、このトークンでスタイリングされる、ということを教えてくれます。ライブラリには欠けている「要素からデザインの決定事項へのマッピング」を補完するものです。

 デザインライブラリKDF
役割"これはボタンです。""これは homepagehero.cta-primary です。"
マッピング汎用的なコンポーネントで、無数に上書きされる可能性がある。1つの要素が1つのデザインキーに紐づく。
追跡可能性要素からデザイン決定までの正確なマップがない。data-kdf によって、すべてのDOMノードがJSONのパスを指し示す。
これらより上のレイヤー shadcn · Bootstrap · Chakra · Tailwind · 素のCSS · 自作システム — KDFはこれらの代わりになるものではありません。
04 — ボキャブラリー

6つの記号。
1つの文法。

KDFはクラス名、共有参照、セクションの順番、そしてCSSカスタムプロパティを保存します。ビジネスロジックやイベントハンドラ、データ取得、権限、アクセシビリティの挙動などは絶対に保存しません。

data-kdf
マップ

トークンを使っているすべての要素は、一致するパスを持っています。DOMノードからJSONまで一直線にたどれるので、スキャナーやテスト、エージェントのレビューに便利です。

data-kdf="hero.title"
d(path)
アクセサー

パスを解決してクラス名の文字列にします。d.css()はCSSカスタムプロパティのオブジェクトを返します。

className={d("hero.title")}
@
共有参照 (Ref)

shared/ の中にある再利用可能なトークンを参照します。Refはチェーンさせたり、追加のクラスで拡張したりできます。

"@button.cta" shadow-xl
$layout
順番と表示

セクションキーの配列です。リストされたセクションは順番通りにレンダリングされ、書かれていないものはホストアプリ側で非表示になります。

["hero", "features", "footer"]
$
コンポーネントの特定

エージェントやホストツールのためのメタデータ。どのコンポーネントがこのトークンをレンダリングするのか、またバリアントやサイズのヒントも含まれます。

"$": "Button"
css
カスタムプロパティ

再利用可能なクラスとして表現できない値を、d.css()を通じてインラインスタイルの変数として適用します。

{ "--kdf-accent": "#4F46E5" }
05 — アーキテクチャ

共通のデフォルト値と
ページごとの上書き。

デザイントークンは2箇所にあります。再利用可能なデフォルト値は shared/ に置き、ページごとの構成で必要な部分だけを上書きします。初回ペイントや細かい調整(エスケープハッチ)には、ユーザー管理の2つのCSSファイルを使います。

プロジェクト構成
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
多層カスケード

@button.cta のような参照は、ページの shared/、次に親の shared/、そしてページのトークンという順に解決されます。テンプレートは必要な部分だけを上書きし、残りはそのまま引き継ぎます。

デフォルトで軽量

デザインはサーバー側で解決され、ブラウザは完成済みのクラス名文字列だけを受け取ります。クリティカルCSSは最初のペイント時に適用され、残りは後から読み込まれるため、低スペックな端末にレンダリングの負荷を押し付けることがありません。

2つのユーザー管理CSS · 2つの読み込みタイミング
konde-server.css
クリティカル · 初回ペイント

アプリからインポートされ、デザイン変数やFOUC(スタイルが当たる前の一瞬の崩れ)を防ぐ上書き設定が、画面に何か表示されるより前の最初のペイント時に届きます。

/* konde-server.css */
:root { --kdf-primary: #1F8F47; }
[data-kdf="hero.slider"] { display: none; }
konde.css
非クリティカル · アプリCSSの後

フレームワークやアプリのCSSの後に読み込まれ、ちょっとした調整や実験、エスケープハッチに使われます。ペイントをブロックする必要のない微調整用です。

/* konde.css */
[data-kdf="hero.title"] { letter-spacing: -0.02em; }
[data-kdf="hero.wrapper"] { gap: 3rem; }

→ KDFはこれらのファイルを一度だけ作成し、勝手に上書きすることはありません。プラグインは環境変数経由でファイルのパスを公開するだけで、インポート自体はあなたのアプリ側で行います。勝手に何かを注入するようなことはしません。

06 — マルチテンプレート

言語を切り替えるように
デザインも切り替える。

i18nが言語を切り替えるように、KDFはデザインテンプレートを切り替えます。アプリもコンポーネントもコードもすべて同じまま、 KDF_DIR で別のデザインフォルダを指定するだけで、全体の見た目がガラッと変わります。

designs/ — 2つのテンプレート、1つのアプリ
designs/
  lander/
    shared/
    homepage.json
  newlander/
    shared/
    homepage.json
// next.config.ts
withKDF({ dir: "./designs/lander" })(nextConfig);
$layout コンポーネントをいじることなく、セクションの並び替えや非表示ができます。
@button 共有トークンを使えば、すべてのCTAボタンを一気に変更できます。
KDF_DIR ホスト側の意図的な決定として、デザインテンプレート全体をスパッと差し替えます。
07 — ランタイムAPI

サーバーで解決して
文字列を渡す。

KDFコアはディスクからJSONを読み込むため、Next.js Server Components、Astro、Honoのハンドラなど、完全にサーバーサイドで動きます。サーバーでクラスを解決して、クライアントのコンポーネントにはただの文字列として渡します。

サーバーコンポーネント
import { getDesign, cn } from "@kondeio/kdf";

const d = getDesign("homepage");

<button data-kdf="hero.cta" className={cn(d("hero.cta"), isActive)}>Start</button>
d("hero.title")

→ 解決済みのclassName文字列

d.css("hero.title")

→ CSSカスタムプロパティのオブジェクト

cn · cx · dedupeClasses

条件付きクラスの結合、falsyな値の削除、重複排除をこなします。デフォルトでは特定のセマンティクスに依存しません。

cache: auto · always · none

本番環境ではキャッシュされます。開発環境ではファイルの更新日時とサイズで再検証します。

08 — 仕様からして互換性あり

手持ちのスタックの
上に乗るだけ。

スタイリング — CSSではなくクラス名を保存

TailwindshadcnBootstrapChakraCSS ModulesPlain CSSYour own system

KDFはCSSエンジンではありません。トークンには、お使いのスタイリングシステムが理解できるクラス文字列がそのまま入ります。もしフレームワークがソースファイルをスキャンする仕様なら、JSONもスキャン対象に指定してください: @source "../kdf/**/*.json"

ランタイム — サーバーサイドJavaScript

Next.js 動作確認済 · プラグインあり
Astro 動作確認済
Hono 動作確認済
はじめに

インストールは1回。
あとはInitにお任せ。

インストール時に、まだ存在しなければ kdf/ フォルダも初期生成してくれます。既存のファイルが上書きされることは絶対にありません。

$npm install @kondeio/kdf
$pnpm add @kondeio/kdf
$bun add @kondeio/kdf
$npm exec -- kdf init # 手動でスキャフォールドする場合
next.config.ts
import withKDF from "@kondeio/kdf/plugin";

export default withKDF({ dir: "./designs" })(nextConfig);