デザインシステム
ranui が拠って立つデザイン言語と、それを表すトークンの完全なカタログです。ライブラリが宣言するすべてのグローバルな --ran-* カスタムプロパティを、両テーマでの値つきで載せています。コンポーネントは値を直に書く代わりにこれらを読むので、トークンをひとつ上書きすれば、それを使うものすべての見た目が変わります。
3 つのページが 3 つの異なる問いに答えます。それらはあえて分けてあります。
| ページ | 答えること |
|---|---|
| デザインシステム(このページ) | トークンが_何か_、つまり語彙 |
| デザインガイドライン | 画面を作るとき、その中から_どう選ぶか_ |
| テーマ | 実行時に_どう切り替え、どう上書きするか_ |
こんなときに:トークンの名前や値(色の役割、余白の段階、アイコンの大きさ、影の段階、イージングのカーブ)が必要なとき、あるいはスケールがなぜこの形をしているのかを知りたいとき。
言語:Geist
ranui のトークンは、Vercel のオープンソースなデザインシステム Geist を土台にしています。どの色スケールも、選ぶための濃淡の集まりではなく、段ごとに役目が定まったはしごです。200 段は「少し暗いグレー」ではなく「ホバーの背景」です。段の役目がいったん定まれば、操作の状態に対する色を選ぶことは判断ではなく引き当てになります。
ranui はそのはしごを --ran-* のスケールとして取り入れ、その上にセマンティックトークンを重ね、既定の書体として Geist Sans / Geist Mono を同梱しています。
ふたつの層
第 1 層:基礎パレット。 下に並ぶ生のスケールです。直接使うことは滅多にありません。
第 2 層:セマンティックトークン。 --ran-color-* とその仲間で、第 1 層の上に対応づけられています。使うのはこの層です。 ダークモードが再定義するのは第 1 層だけなので、すべてのセマンティックトークンは var() を通じて切り替わり、ライブラリのどこにもコンポーネントごとのダーク用の上書きはありません。
--ran-gray-1000 → #171717(ライト) / #ededed(ダーク) ← 第 1 層。切り替わる
--ran-color-text → var(--ran-gray-1000) ← 第 2 層。追随する
--ran-btn-color → var(--ran-color-text, …) ← コンポーネントのトークンこの連鎖がアーキテクチャのすべてです。基礎の段を変えればどこにでも伝わり、セマンティックトークンを変えればひとつの役割が変わり、コンポーネントのトークンを変えればひとつの要素が変わります。
色
はしご
どの色相のスケールも 100 → 1000 で走り、段ごとに役目がひとつ定まっています。
| 段 | 役目 | 段 | 役目 |
|---|---|---|---|
| 100 | 既定の背景 | 600 | アクティブの境界線 |
| 200 | ホバーの背景 | 700 | ベタ塗り(ボタン/バッジ) |
| 300 | アクティブ(押下)の背景 | 800 | ベタ塗り(ホバー) |
| 400 | 既定の境界線 | 900 | 副次のテキストとアイコン |
| 500 | ホバーの境界線 | 1000 | 主要なテキストとアイコン |
背景
| トークン | ライト | ダーク | 用途 |
|---|---|---|---|
--ran-background-100 |
#ffffff |
#000000 |
ページの背景 |
--ran-background-200 |
#fafafa |
#000000 |
控えめな区画 |
グレー — --ran-gray-100..1000
テキスト、境界線、面の背後にあるスケールです。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #f2f2f2 |
#1a1a1a |
| 200 | #ebebeb |
#1f1f1f |
| 300 | #e6e6e6 |
#292929 |
| 400 | #eaeaea |
#2e2e2e |
| 500 | #c9c9c9 |
#454545 |
| 600 | #a8a8a8 |
#878787 |
| 700 | #8f8f8f |
#8f8f8f |
| 800 | #7d7d7d |
#7d7d7d |
| 900 | #4d4d4d |
#a0a0a0 |
| 1000 | #171717 |
#ededed |
グレー(アルファ) — --ran-gray-alpha-100..1000
半透明なので、どんな面の上にも重ねられます。覆いの膜、ホバーの淡い色、そして何が下にあるか分からない場所に置く区切り線には、これが正解です。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #0000000d |
#ffffff12 |
| 200 | #00000015 |
#ffffff17 |
| 300 | #0000001a |
#ffffff21 |
| 400 | #00000014 |
#ffffff24 |
| 500 | #00000036 |
#ffffff3d |
| 600 | #0000003d |
#ffffff82 |
| 700 | #00000070 |
#ffffff8a |
| 800 | #00000082 |
#ffffff78 |
| 900 | #000000b3 |
#ffffff9c |
| 1000 | #000000e8 |
#ffffffeb |
ブルー — --ran-blue-100..1000
リンクとフォーカスリングのために取ってあります。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #f0f7ff |
#06193a |
| 200 | #e9f4ff |
#022248 |
| 300 | #dfefff |
#002f62 |
| 400 | #cae7ff |
#003674 |
| 500 | #94ccff |
#00418b |
| 600 | #48aeff |
#0090ff |
| 700 | #006bff |
#006efe |
| 800 | #0059ec |
#005be7 |
| 900 | #005ff2 |
#47a8ff |
| 1000 | #002359 |
#eaf6ff |
レッド — --ran-red-100..1000
危険とエラーです。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #ffeeef |
#330a11 |
| 200 | #ffe8ea |
#440d13 |
| 300 | #ffe3e4 |
#5d0e17 |
| 400 | #ffd7d6 |
#6f101b |
| 500 | #ffb1b3 |
#88151f |
| 600 | #ff676d |
#f32e40 |
| 700 | #fc0035 |
#f13242 |
| 800 | #ea001d |
#e2162a |
| 900 | #d8001b |
#ff565f |
| 1000 | #47000c |
#ffe9ed |
アンバー — --ran-amber-100..1000
警告です。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #fff6de |
#2a1700 |
| 200 | #fff4cf |
#361900 |
| 300 | #fff1c1 |
#502800 |
| 400 | #ffdc73 |
#5b3000 |
| 500 | #ffc543 |
#703e00 |
| 600 | #ffa600 |
#ed9a00 |
| 700 | #ffae00 |
#ffae00 |
| 800 | #ff9300 |
#ff9300 |
| 900 | #aa4d00 |
#ff9300 |
| 1000 | #561900 |
#fff3d5 |
グリーン — --ran-green-100..1000
成功です。
| 段 | ライト | ダーク |
|---|---|---|
| 100 | #ecfdec |
#002608 |
| 200 | #e5fce7 |
#00320b |
| 300 | #d3fad1 |
#003a0e |
| 400 | #b9f5bc |
#004615 |
| 500 | #82eb8d |
#006717 |
| 600 | #4ce15e |
#00952d |
| 700 | #28a948 |
#00ac3a |
| 800 | #279141 |
#009432 |
| 900 | #107d32 |
#00ca50 |
| 1000 | #003a00 |
#d8ffe4 |
セマンティックな色トークン
コンポーネントが実際に読む層です。ここにあるものはすべて上のスケールを通じて解決されるので、テーマに合わせて自分で切り替わります。
| トークン | 解決先 | 役割 |
|---|---|---|
--ran-color-bg |
--ran-background-100 |
ページの背景 |
--ran-color-bg-subtle |
--ran-background-200 |
控えめな区画 |
--ran-color-bg-elevated |
--ran-background-100 · gray-100 (ダーク) |
カード、面 |
--ran-color-bg-muted |
--ran-gray-100 |
沈んだ/控えめな塗り |
--ran-color-bg-hover |
--ran-gray-200 |
ホバーの面 |
--ran-color-bg-active |
--ran-gray-300 |
アクティブ(押下)の面 |
--ran-color-text |
--ran-gray-1000 |
主要なテキスト |
--ran-color-text-secondary |
--ran-gray-900 |
副次のテキスト |
--ran-color-text-disabled |
--ran-gray-700 |
無効なテキスト |
--ran-color-border |
--ran-gray-400 |
既定の境界線 |
--ran-color-border-secondary |
--ran-gray-300 |
より控えめな境界線 |
--ran-color-border-hover |
--ran-gray-500 |
ホバーの境界線 |
--ran-color-border-active |
--ran-gray-600 |
アクティブの境界線 |
--ran-color-primary |
--ran-gray-1000 |
主たるアクション(モノクロ) |
--ran-color-primary-hover |
#383838 · #cccccc (ダーク) |
primary のホバー |
--ran-color-primary-active |
#4d4d4d · #b3b3b3 (ダーク) |
primary の押下 |
--ran-color-primary-text |
--ran-background-100 |
primary の面の上に乗るインク |
--ran-color-success |
--ran-green-700 |
成功 |
--ran-color-warning |
--ran-amber-700 |
警告 |
--ran-color-danger |
--ran-red-700 |
危険/エラー |
--ran-color-link |
--ran-blue-700 |
リンク |
--ran-color-primary-hover / -active は、セマンティック層にあるふたつのリテラルです。スケールに沿ってではなくページの背景に向かって進むので、ダークモードではこれらを直接定義し直します。
アクセントそれぞれの意味
- primary はモノクロです:ライトでは白地に黒、ダークでは黒地に白(Geist のブランドの調子で、
<r-button type="primary">)。その上のテキストとアイコンは--ran-color-primary-textを使い、これも一緒に切り替わります。別に「コントラスト」のトークンはありません。primary こそが 最もコントラストの高いアクションです。 - ブルーは取ってあります:リンク(
--ran-color-link)とフォーカスリングのためです。primary の代わりではありません。 - グリーン=成功 · アンバー=警告 · レッド=危険。 それぞれ意味はひとつです。
--ran-color-error はありません。トークンは --ran-color-danger です。宣言されていないプロパティを指す var() は何にも解決されず、宣言まるごとが黙って捨てられます。だから名前を間違えたときは、当て推量ではなくこの表と突き合わせる価値があります。
余白
ものとものの間隔、つまり padding、margin、gap です。基本単位は 4px で、値は 9 つ、それ以上はありません。
| トークン | 値 | トークン | 値 |
|---|---|---|---|
--ran-space-1 |
4px | --ran-space-8 |
32px |
--ran-space-2 |
8px | --ran-space-10 |
40px |
--ran-space-3 |
12px | --ran-space-16 |
64px |
--ran-space-4 |
16px | --ran-space-24 |
96px |
--ran-space-6 |
24px |
数字は 4px の倍数なので、スケールは飛び飛びです。--ran-space-5 はありません。そこが肝心で、ページのリズムを生むのは限られた選択肢のほうです。
寸法
要素そのものの大きさです。アイコンの大きさ、コントロールの高さ、小さな正方形や長方形のコントロールなど。
| トークン | 値 | 典型的な用途 |
|---|---|---|
--ran-size-1 |
16px | チェックボックスの箱、小さな行内アイコン |
--ran-size-2 |
18px | — |
--ran-size-3 |
20px | コントロール内のアイコン |
--ran-size-4 |
24px | ツールバーのアイコンボタン |
--ran-size-5 |
28px | 詰まったコントロールの高さ |
--ran-size-6 |
30px | — |
--ran-size-7 |
32px | 既定のコントロールの高さ |
これは意図的に余白とは別のスケールです。 混ぜて使うのは機械が検出するエラーです(sizing-scale)。ふたつは範囲も刻み方も違います(4px を倍にしていく余白のスケールは、アイコンやコントロールの大きさとしては据わりの悪い値になります)。そして利用者は、片方を調整しても、もう片方を乱さずにいられなければなりません。アイコンが大きくなったからといって、たまたま同じピクセル値を共有していた余白まで広がってしまってはいけないのです。段が余白の段と数字の上で一致する場合(--ran-size-4 と --ran-space-6 はどちらも 24px です)、それは偶然であって別名ではありません。
ほかのどのコンポーネントとも共有しない、本当に一回きりの寸法(たとえばメニューの min-width)は、無理に段へ押し込まず、自前のリテラルなフォールバックを持つ素のコンポーネントトークンのままにします。
タイポグラフィ
| トークン | 値 |
|---|---|
--ran-font-family |
Geist / Geist Sans、次いでシステム UI のスタック |
--ran-font-mono |
Geist Mono、次いで ui-monospace、SF Mono、Menlo、Consolas… |
--ran-font-size |
14px(基準の大きさ) |
--ran-line-height |
1.5715 |
文字は役割で整理され、役割がフォント・大きさ・太さ・行間をまとめて決めます。
| 役割 | 用途 | 太さのトークン | 大きさのトークン |
|---|---|---|---|
| heading | 見出し | --ran-text-heading-weight (600) |
--ran-text-heading-1..4 (32/24/20/16px) |
| label | 一行で、目で追えるもの | --ran-text-label-weight (500) |
--ran-text-label-1..3 (14/13/12px) |
| copy | 複数行の本文 | --ran-text-copy-weight (400) |
--ran-text-copy-1..2 (16/14px) |
| button | ボタンの文字 | --ran-text-button-weight (500) |
--ran-text-button-size (14px) |
| mono | コード、データ、小見出し | --ran-text-mono-weight-regular (400) / --ran-text-mono-weight-medium (500) |
label / copy の大きさを借ります |
役割をきちんと着地させるためだけに、ふたつのトークンがあります。
| トークン | 値 | 理由 |
|---|---|---|
--ran-text-heading-tracking |
-0.03em |
大きな表示サイズでは、見出しに詰めた字間が要ります。 |
--ran-text-button-line-height |
1 |
高さの決まったコントロールの中で、縦位置をきれいに揃えます。 |
Geist は太さの上限が 600(セミボールド)です。強調は大きさと余白から生まれるもので、より太い書体からではありません。--ran-text-copy-3 はありません。12px の段は --ran-text-label-3 です。
フォント
ranui はどちらの書体も自前でホストしています(可変ウェイト 100〜900、SIL OFL 1.1)。だから import ひとつで、CDN への依存なしに読み込めます。
import 'ranui/fonts'; // バンドラー向け<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />これがなければトークンはシステムのフォントスタックへフォールバックします。すべて問題なく動きますが、Geist の書体にはなりません。
角丸
| トークン | 値 | 用途 |
|---|---|---|
--ran-radius-sm |
6px |
コントロール:ボタン、入力、セレクト |
--ran-radius-md |
12px |
カード、ダイアログ |
--ran-radius-lg |
16px |
大きな面 |
--ran-radius-full |
9999px |
ピル、アバター |
影の高さ
影は装飾ではなく役割です。段階は、その要素が何であるかで選びます。ダークモードでは 3 つとも差し替えられます。白いページに合わせて調整した影は、黒いページでは消えてしまうからです。
| トークン | 用途 | ライト | ダーク |
|---|---|---|---|
--ran-shadow-elevated |
流れの中にあって境界線も持つ面:r-card、r-section |
0 1px 2px rgba(0,0,0,.04), 0 2px 4px -2px rgba(0,0,0,.05) |
0 1px 2px rgba(0,0,0,.16) |
--ran-shadow-menu |
内容の上に一時的に重なる層:ドロップダウン、セレクト、ポップオーバー、トースト | 0 2px 4px rgba(0,0,0,.05), 0 8px 24px -6px rgba(0,0,0,.14) |
0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5) |
--ran-shadow-modal |
行く手をふさぐダイアログ:r-modal |
0 4px 12px rgba(0,0,0,.08), 0 20px 48px -12px rgba(0,0,0,.22) |
0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5) |
境界線のないオーバーレイは、周囲との切り分けを影だけに頼ります。だからオーバーレイの段階には本当に重みがあります。オーバーレイが持ち上げの段階に落ちてしまうと、平らでページに貼り付いて見えます。
重なり
浮くオーバーレイは <body> へポータルされるので、明示的な段階が要ります。
| トークン | 既定値 | 用途 |
|---|---|---|
--ran-z-modal |
1000 |
行く手をふさぐダイアログとそのマスク |
--ran-z-dropdown |
1100 |
ドロップダウン/セレクト/ポップオーバー:モーダルの上。ダイアログの中のセレクトが見えたままになります |
--ran-z-message |
1200 |
トーストと通知:常に最前面 |
はしごが 1000 から始まるのは、普通のページの外枠を越えるためです(ナビゲーションバーや背景は、たいてい十の位に置かれます)。段階を上書きするときは :root で、あるいはコンポーネントごとに(--ran-dropdown-host-z-index、--ran-modal-root-z-index、--ran-message-z-index)行い、!important は決して使わないでください。
モーション
| トークン | 値 | 用途 |
|---|---|---|
--ran-motion-duration-fast |
0.15s |
ホバー/アクティブの状態遷移 |
--ran-motion-duration-base |
0.2s |
ポップオーバー、メニュー |
--ran-motion-duration-slow |
0.35s |
もっと大きな現れ方 |
| イージングのトークン | カーブ | 性格 |
|---|---|---|
--ran-motion-ease-standard |
cubic-bezier(0.645,0.045,0.355,1) |
イン・アウト。汎用 |
--ran-motion-ease-snappy |
cubic-bezier(0.33,0,0.15,1) |
素早く、行き過ぎなし:トグル |
--ran-motion-ease-spring |
cubic-bezier(0.34,1.26,0.5,1) |
わずかに行き過ぎ:ボタン、カード |
--ran-motion-ease-bouncy |
cubic-bezier(0.34,1.56,0.64,1) |
遊びのある行き過ぎ:いいね、カートに追加 |
--ran-motion-ease-smooth |
cubic-bezier(0.4,0,0.2,1) |
穏やかで行き過ぎなし:現れ、レイアウト |
spring 系は、調整済みの SwiftUI のばねから蒸留したものです(response / damping を、一度だけ行き過ぎるベジェに落とし込んでいます)。
これらは動きのプロパティとだけ組み合わせてください:transform、opacity、箱の寸法です。パレットのプロパティ(background-color、color、border-color、box-shadow、fill、stroke)には、あえて既定のトランジションを付けていません。CSS は操作とテーマの切り替えを区別できないからです。色に付けたフェードは、ライトとダークが入れ替わるときにも発火します。それでも自分で有効にしたいときのために、どのコンポーネントも --ran-*-transition のフックを公開しています。
フォーカス
| トークン | 値 | 用途 |
|---|---|---|
--ran-focus-ring |
0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700) |
標準のリング。box-shadow として |
--ran-focus-ring-inverse-color |
#fff |
どちらの テーマでも暗いままの面のための、リングの色 |
リングは 2 層です。背景色の内側のリングと、青い外側のリング。だからどんな面の上でも見えたままになりますし、モノクロになった primary に追随せず青のままです。
--ran-focus-ring-inverse-color はあえてダークモードで再定義していません。ページのテーマに関わらず自分の面が暗いままのコンポーネント(任意の映像の上に重なる r-player のコントロールバー)のためにあり、その面はページが変わっても変わらないからです。
スキンのプリミティブ
コンポーネントが共有する構造的な値のうち、色でも寸法でも文字でもない、ごく少数のものです。あえて最小限に保っています。この層はかつてもっと大きく、そのほとんどはテーマパックとともに取り除かれました。
| トークン | 値 | 用途 |
|---|---|---|
--ran-skin-border-width |
1px |
コンポーネントが描く境界線の太さ |
--ran-skin-border-style |
solid |
コンポーネントが描く境界線の種類 |
--ran-skin-border-image-width |
4px |
border-image-slice のインセット。button / checkbox / input / modal / message が共有します |
--ran-skin-raised-shadow |
var(--ran-shadow-elevated) |
持ち上げた面の影。スキンが変えられるよう間接参照にしてあります |
--ran-skin-font-family |
var(--ran-font-family) |
コンポーネントが使う書体。同じように間接参照にしてあります |
ダークモードが再定義するもの
<html>(あるいは任意のサブツリー。テーマを参照)に付いた data-ran-theme="dark" が再定義するのは、基礎パレットだけです。ただし、スケールを通じては解決できない例外が 3 つあります。
- 第 1 層のすべて:グレー、グレー(アルファ)、ブルー、レッド、アンバー、グリーンの全段と、ふたつの背景。
--ran-color-bg-elevated。ダークでは--ran-gray-100を指し、カードが黒いページに溶けるのではなく浮き上がるようにします。--ran-color-primary-hover/-active。スケールの参照ではなくリテラルだからです。- 影の 3 段階すべて。暗い地に合わせて調整し直されます。
それ以外(ほかのすべてのセマンティックトークン、すべての寸法、すべての時間)は一度だけ定義されます。
コンポーネントのトークン
セマンティック層の下では、どのコンポーネントも自前のフックを、こう名づけて公開しています。
--ran-{component}-{element}[-{state}]-{property}たとえば --ran-btn-hover-background、--ran-select-search-active-border-width です。既定ではセマンティックトークンへ落ちます:var(--ran-btn-background, var(--ran-color-primary, #171717))。だからセマンティックトークンをひとつ上書きすればそのすべてに届き、コンポーネントのトークンを上書きすれば変化はひとつの要素に絞られます。
生成された完全な一覧は、リポジトリの style-tokens-public.md にあります。要素ごとの API はこちらです。適用のしかたはテーマを参照してください。
自分の CSS でトークンを使う
.panel {
background: var(--ran-color-bg-elevated);
color: var(--ran-color-text);
border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border);
border-radius: var(--ran-radius-md);
padding: var(--ran-space-4);
box-shadow: var(--ran-shadow-elevated);
}3 つのルールが、それをダークでも安全に保ちます。
- テーマに追随すべきものに生の 16 進数を書かないこと。
- フォールバックは切り替わるトークンを指すこと:
var(--ran-color-text, var(--ran-gray-1000))であって、var(--ran-color-text, #171717)ではありません。 - フォールバックは存在するトークンを指すこと。さもないと宣言は捨てられ、要素は継承したものを黙って保ち続けます。
ライブラリが宣言するグローバルなトークンはすべてこのページに載っており、ここに書かないままトークンを増やすとユニットテストが落ちます。コンポーネント単位のトークンは別途生成され、style-tokens-public.md にあります。