Design System

Kotoba Design System — tokens, primitives and the rules behind them

トークンの節は src/styles/globals.css @theme をビルド時に読んで組み立てている。値をここに書き写して いないので、CSS を触ればこのページも同じだけ動く。

原則

  1. 1
    プライマリの面は黒

    塗るのは --color-gray-900 で、アクセントではない。1 画面に塗り面は 1 つ。

  2. 2
    アクセントはフォーカスと選択だけ

    rgb(242 49 130) を塗り・ステータス・装飾に使わない。ステータスは色ではなく言葉と位置で示す。

  3. 3
    文字は 0.875rem (14px) が上限

    見出しも本文も同じ 14px。階層はサイズではなく太さ・色・余白で作る。上の段は存在しない。

トークン

13 節 / 103 トークン。並びは globals.css の宣言順のままで、節の前置きも CSS 側のコメントを そのまま持ち上げている。

ニュートラルランプ

bg-gray-* / text-gray-* / border-gray-*
  • ニュートラルランプ

    アクセント以外のすべての色 — 面も境界もテキストも、そしてプライマリアクションの塗りもここが担う。Tailwind 既定の gray を先に消してあるので、梯子に無いシステム外の灰色が紛れ込むことはない。

    gray-*

    initial

    Tailwind 既定のスケールを消している。この名前空間はここで定義した段しか持たない

  • gray-0

    #ffffff

  • gray-25

    #fafafb

  • gray-50

    #f4f4f6

  • gray-100

    #ebebee

  • gray-200

    #dedee3

  • gray-300

    #c3c3cb

  • gray-400

    #9a9aa3

  • gray-500

    #74747c

  • gray-600

    #56565e

  • gray-800

    #2c2c31

  • gray-900

    #16161a

アクセント

bg-accent / text-accent / border-accent
  • 唯一の有彩色

    4 段だけ。使ってよいのはフォーカスリングと選択状態の 2 箇所のみ。基準は src/assets/logo.png のピンクそのもの (#f23182)。600 / 700 / tint は同じ色相のまま、明度と彩度だけを動かした段で、白地に対する 3:1 (非文字のコントラスト下限) は基準の 3.79 で満たしている。

    accent

    rgb(242, 49, 130)

  • accent-600

    #cb2a6e

  • accent-700

    #9b2356

  • accent-tint

    #feeff5

セマンティックカラー

bg-* / text-* / border-*
  • セマンティック: 面

    background

    var(--color-gray-0)

    = #ffffff

  • foreground

    var(--color-gray-900)

    = #16161a

  • card

    var(--color-gray-0)

    = #ffffff

  • card-foreground

    var(--color-gray-900)

    = #16161a

  • popover

    var(--color-gray-0)

    = #ffffff

  • popover-foreground

    var(--color-gray-900)

    = #16161a

  • inverse

    var(--color-gray-900)

    = #16161a

  • inverse-foreground

    var(--color-gray-0)

    = #ffffff

  • セマンティック: 静かな面と二次テキスト

    --color-muted はこのシステムで唯一のティント。

    muted

    var(--color-gray-50)

    = #f4f4f6

  • muted-foreground

    var(--color-gray-500)

    = #74747c

  • subtle

    var(--color-gray-50)

    = #f4f4f6

  • subtle-foreground

    var(--color-gray-600)

    = #56565e

  • secondary

    var(--color-gray-50)

    = #f4f4f6

  • secondary-foreground

    var(--color-gray-900)

    = #16161a

  • セマンティック: プライマリアクション — 有彩色ではなく黒

    primary

    var(--color-gray-900)

    = #16161a

  • primary-foreground

    var(--color-gray-0)

    = #ffffff

  • primary-pressed

    var(--color-gray-800)

    = #2c2c31

  • セマンティック: 破壊的操作

    このシステムに色分けされたステータスは無い。失敗状態は色相ではなく「言葉と位置」で表す。既存の `bg-destructive` が解決できるようキーは残してあるが、描かれるのはプライマリの黒。

    destructive

    var(--color-gray-900)

    = #16161a

  • destructive-foreground

    var(--color-gray-0)

    = #ffffff

  • セマンティック: 境界

    エレベーションは存在しない。分離は 1px のヘアラインか余白で行う。

    border

    var(--color-gray-200)

    = #dedee3

  • border-hairline

    var(--color-gray-100)

    = #ebebee

  • border-strong

    var(--color-gray-300)

    = #c3c3cb

  • input

    var(--color-gray-300)

    = #c3c3cb

  • セマンティック: フォーカスと選択 — アクセントを使う唯一の場所

    ring

    var(--color-accent)

    = rgb(242, 49, 130)

  • selected

    var(--color-accent-tint)

    = #feeff5

  • selected-border

    var(--color-accent)

    = rgb(242, 49, 130)

  • セマンティック: 無効状態

    disabled

    var(--color-gray-100)

    = #ebebee

  • disabled-foreground

    var(--color-gray-400)

    = #9a9aa3

文字の太さ

font-*
  • 太さは 4 段だけ。書体は可変 (100-900) だが、システムとして出荷するのはこの 4 つ。既定を消してあるので font-light / font-black は効かない。サイズが 1 つに寄っているぶん、階層はここが担う。

    *

    initial

    Tailwind 既定のスケールを消している。この名前空間はここで定義した段しか持たない

  • Aa あア 012
    normal

    400

  • Aa あア 012
    medium

    500

  • Aa あア 012
    semibold

    600

  • Aa あア 012
    bold

    700

書体

font-*
  • Font — 本文も見出しもコードも等幅で通す。欧文は Noto Sans Mono、和文は Noto Sans JP。同じ Noto なので骨格とウェイトの刻みが揃う。faces は self-host し、配信経路は 2 通りに分かれる。

    欧文は src/styles/fonts.ts の next/font で宣言し、Next が family 名をハッシュ付きに書き換えるので、ここでは名前ではなく next/font が出す CSS 変数を参照する。変数の中身は 実書体 → 和文 → system mono の順で、書体が届く前も等幅のまま落ちる (代替を等幅に寄せている理由は fonts.ts のコメント)。変数を張るのは <html> なので、body 直下のポータルにも届く。

    CJK の "Noto Sans JP Variable" だけは Fontsource の CSS をそのまま読むので名前で書く (Fontsource の可変フォントは "… Variable" という別名で登録されるため、静的版の名前とは互換ではない)。Storybook は Next を通らないので、.storybook/preview.ts が @fontsource-variable/* を直接 import してこの 2 書体を用意する。

    sans と mono が同じ値を指すのは意図的。役割としての 2 つは残しつつ、実体を 1 つの等幅書体に寄せている。

    Aa あア 012
    sans

    var(--font-noto-sans-mono, ui-monospace), monospace

  • Aa あア 012
    mono

    var(--font-noto-sans-mono, ui-monospace), monospace

サイズスケール

text-*
  • サイズスケール 4 段。コメントの px はルート 16px 時の実寸。 base が基準であり上限で、この上に段は無い。

    *

    initial

    Tailwind 既定のスケールを消している。この名前空間はここで定義した段しか持たない

  • Aa あア
    2xs

    0.6875rem

    line-height: 1rem

    letter-spacing: var(--tracking-wider) = 0.04em

  • Aa あア
    xs

    0.75rem

    line-height: 1rem

    letter-spacing: var(--tracking-wide) = 0.02em

  • Aa あア
    sm

    0.8125rem

    line-height: 1.25rem

    letter-spacing: var(--tracking-normal) = 0em

  • 基準サイズ。本文も見出しもここに乗る。スケールの上限でもある。

    Aa あア
    base

    0.875rem

    line-height: 1.25rem

    letter-spacing: var(--tracking-normal) = 0em

  • 文字組みの外側。絵文字や数字を「文字」ではなく「絵」として置く 1 箇所だけのための逃がし口で、テキストの段ではない (だから 2xs…base の梯子に連なる名前を付けていない)。現状の使用箇所はトップの ☕️ と 404 の数字の 2 つだけ。コピーには使わない。

    Aa あア
    mark

    2rem

    line-height: var(--leading-none) = 1

    letter-spacing: var(--tracking-tighter) = -0.025em

行間

leading-*
  • 行間。段ごとの既定は各 --text-*--line-height にあり、これは長文などで明示的に上書きするための語彙。relaxed が和文の長文用。

    あいうえお かきくけこ さしすせそ
    none

    1

  • あいうえお かきくけこ さしすせそ
    tight

    1.25

  • あいうえお かきくけこ さしすせそ
    snug

    1.4

  • あいうえお かきくけこ さしすせそ
    normal

    1.5

  • あいうえお かきくけこ さしすせそ
    relaxed

    1.75

  • あいうえお かきくけこ さしすせそ
    loose

    2

字間

tracking-*
  • 字間。widest だけは大文字組み (overline) 専用。

    Aa あア 012
    tighter

    -0.025em

  • Aa あア 012
    tight

    -0.015em

  • Aa あア 012
    normal

    0em

  • Aa あア 012
    wide

    0.02em

  • Aa あア 012
    wider

    0.04em

  • Aa あア 012
    widest

    0.1em

余白の基本単位

p-4 / gap-2 … の 1 単位
  • 余白の基本単位。出荷される余白の値はすべて 4 × n

    spacing

    4px

余白スケール

p-* / m-* / gap-* / h-*
  • 余白の tier

    tier 1 は Screen の専有、tier 2 は兄弟要素のあいだ、tier 3 はコンポーネントの内側にあり margin として外に漏れない。

    tier 1 は 1 本の梯子ではなく軸ごとに読む。左右 (24) がブロック間 (32) より小さいのは、読み幅がもともと狭く、ガターを広げたぶんがそのまま行長から引かれるため。上下 (56 / 48) がブロック間より大きいのは、ヘッダーを持たないこのサイトではページの上端が兄弟要素ではなくブラウザの UI と接するから。

    tier 3 だけは軸によらない 1 つの規則で、要素の内側は 20 を超えない。

    `inline-block` というクラスは書かない。** Tailwind はこの名前空間から `inline-size` 用の `inline-<余白キー>` ユーティリティを作るので、 `--spacing-block` が存在した時点で `inline-block` は `display: inline-block` と `inline-size: 32px` の両方を吐く。しかも `inline-size` は同じ要素の `w-*` に勝つため、箱が黙って 32px 幅に潰れてラベルがはみ出す。中身の幅に縮む inline の箱には `inline-flex` を使う。 `block` 単体は安全 (`block-*` = block-size のユーティリティは無い)。

    edge-h

    24px

  • edge-top

    56px

  • edge-bottom

    48px

  • block

    32px

  • block-loose

    48px

  • block-tight

    20px

  • inset-x

    20px

  • inset-y

    12px

  • gap

    8px

  • gap-tight

    4px

  • タップ形状

    視覚的な箱とタップ領域は別のトークン。ラベルのサイズを変えても箱は動かない。

    tap-min

    44px

  • control

    40px

  • control-lg

    52px

  • hitslop

    2px

  • アイコン

    グリフの一辺。13px を px ではなく rem で持つのは、ブラウザの既定文字サイズを上げたときにテキストだけが伸びてグリフが取り残されないようにするため (隣に並ぶ text-sm と同じ 0.8125rem)。余白ではないが、Tailwind の size-* が読むのは spacing の名前空間なのでここに置く。

    icon

    0.8125rem

角丸

rounded-*
  • 角丸

    コントロールは 12、カードは 16、フォーカスリングは 14 (コントロール + オフセット 2px)、ピルは 999。これ以外の値は使わない。

    sm

    8px

  • md

    12px

  • lg

    16px

  • control

    12px

  • card

    16px

  • focus

    14px

  • chip

    999px

イージング

ease-*
  • モーション

    120ms、color と opacity のみ。バウンスもスプリングも無く、中身を拡大縮小させることもしない。

    standard

    cubic-bezier(0.2, 0, 0.2, 1)

アニメーション

animate-*
  • アニメーション

    fade-in

    0.2s ease fade-in

  • fade-out

    0.2s ease fade-out

  • scale-in

    0.2s ease scale-in

  • scale-out

    0.2s ease scale-out

  • slide-up

    0.5s ease slide-up

  • slide-left

    0.3s ease slide-left

  • fade-scale-in

    var(--animate-fade-in), var(--animate-scale-in)

    = 0.2s ease fade-in, 0.2s ease scale-in

  • fade-scale-out

    var(--animate-fade-out), var(--animate-scale-out)

    = 0.2s ease fade-out, 0.2s ease scale-out

  • fade-slide-up

    var(--animate-fade-in), var(--animate-slide-up)

    = 0.2s ease fade-in, 0.5s ease slide-up

  • fade-slide-left

    var(--animate-fade-in), var(--animate-slide-left)

    = 0.2s ease fade-in, 0.3s ease slide-left

コンポーネント

src/components/ui/ のプリミティブ。すべて上のトークンだけで 組まれていて、固有の色やサイズを持たない。

Text

8 つの role。上の 5 つは同じ 14px で、違うのは太さと色だけ。

  • title

    見出しも本文も 14px — Aa あア 012

  • heading

    見出しも本文も 14px — Aa あア 012

  • subheading

    見出しも本文も 14px — Aa あア 012

  • body

    見出しも本文も 14px — Aa あア 012

  • lead

    見出しも本文も 14px — Aa あア 012

  • caption

    見出しも本文も 14px — Aa あア 012

  • label
    見出しも本文も 14px — Aa あア 012
  • overline

    見出しも本文も 14px — Aa あア 012

PageHeader

Notes

書いたものの置き場

Button

面の高さ (40 / 52) とタップ領域 (最低 44) は別のトークン。押下は塗りの差し替えで、 縮小も不透明度も使わない。

Card

カードのタイトル

説明は 13 / 400。面と地の差は 1px の境界だけで、影は無い。

本文。カードの角は --radius-card (16px) で、コントロールの 12px とは別のトークン。

Dialog

Command

Toast

Timeline

  1. 公開した

    2025-11-01

  2. 下書きにした

    2025-11-02

  3. 書きはじめた

    2025-11-03

Screen

端末幅 390px を模した器。画面端の余白 (24 / 32 / 24) を宣言してよい唯一の場所。

Unit 4 · Requests

お願いできますか

onegai dekimasu ka

パターン

プリミティブではなく、globals.css が直接持っているふるまい。

フォーカスリング

アクセントが許されている 2 箇所のうちの 1 つ。2px を 2px 外側に置くので、角丸は コントロールの 12px + 2 = --radius-focus (14px)。下の 2 つを Tab で辿ると出る。

リンク

記事本文 (.note-content)

MDX の本文に当たるスタイル。見出しの 4 段はサイズを動かさず、太さ・色・直上の余白 だけで段を作る。

h1 — 14 / 700

本文は 14px の行間 1.75。リンクは色を変えず下線の太さで 示し、インラインコードは面で区別する。

h2 — 14 / 600

段落の間隔は --spacing-block-tight (20px)。

h3 — 14 / 600 + 弱い色

  • 順不同リスト
  • マーカーは muted-foreground

h4 — 14 / 500 + 弱い色

  1. 順序付きリスト
  2. 字下げは --spacing-inset-x (20px)
引用は左の 2px 罫と字下げで示す。色は subtle-foreground。
const spacing = 4 // px
役割
境界1px hairline
無し

区切りは 1px のヘアライン。

ユーティリティ

@layer utilities の 4 つ。animate-* トークンと組み合わせて、登場を少しずらす。

遅延なし
animation-delay-200
animation-delay-400
animation-delay-600