デザインガイドライン
ranui のコンポーネントで組み立てた画面が、部品の寄せ集めではなくひとつのシステムとして読めるように従うべきルールです。
このページが扱うのは判断です。どのトークンに手を伸ばすか、出す前に何を確かめるか。トークンのカタログそのものはデザインシステム、実行時の切り替えと上書きはテーマにあります。これらのルールを機械的に強制する完全版は、リポジトリの packages/ranui/docs/DESIGN.md にあります。
こんなときに:ページをレイアウトしていたり、
<r-*>要素からアプリ側のコンポーネントを組み立てていて、色・余白・文字の大きさ・影・アニメーションの長さを決めなければならないとき。短い答えはいつも同じです。役割を選び、値はトークンに任せること。
原則
- 個性より明快さを。 主たるタスクと主たるアクションは、ほかの何を考えるより先に、疑いようがなくなっていなければなりません。
- 組み合わせる。作り直さない。
divからプリミティブを作る前にr-button、r-input、r-select、r-modalに手を伸ばしてください。フォーカス、キーボード、ARIA のふるまいは、それらのコンポーネントがすでに備えています。 - 生の値ではなくトークンを。 16 進数のコード、
20pxの余白、手で選んだ影は、テーマに追随しない決定です。 - 目分量ではなく、役割と状態で決める。 「このテキストは何か」(見出し/ラベル/本文/ボタン)には答えがありますが、「どの大きさが良く見えるか」には答えがありません。
- たどり着けるすべての状態を設計する。 既定、ホバー、アクティブ、フォーカス、無効、読み込み中、空、エラー。既定は 8 つのうちのひとつにすぎません。
- 描画されたものを検証する。 ライト_と_ダーク、狭い_と_広い、マウス_と_タッチで。レビューでは、見えない影は捕まえられません。
ふたつのルールが引っ張り合うときの優先順は、ユーザーの目的 → 検証された証拠 → このガイドライン → 出荷済みのパターン → 一般的な経験則です。
色の選び方
色は役割と状態で割り当てるもので、目で選ぶものではありません。ホバーとアクティブの見た目ははしごがすでに決めています。あなたの仕事は役割に名前を付けることです。
| この要素は… | 使うもの |
|---|---|
| ページや面の背景 | --ran-color-bg / -bg-subtle / -bg-elevated / -bg-muted |
| ポインターの下/押されている | --ran-color-bg-hover / -bg-active |
| テキスト | --ran-color-text / -text-secondary / -text-disabled |
| 境界線 | --ran-color-border / -hover / -active |
| この画面が存在する理由のアクション | --ran-color-primary(その上に --ran-color-primary-text) |
| 状態 | --ran-color-success / -warning / -danger |
| リンク | --ran-color-link |
アクセントの意味はひとつずつです。 primary はモノクロ(ライトでは白地に黒、ダークでは黒地に白)なので、そこに青を使わないでください。青はリンクとフォーカスリングのものです。緑は成功、琥珀色は警告、赤は危険。強調に赤を使ってしまうと、あとで危険を表す色がなくなります。
黙って壊れるのを防ぐ 3 つのルール:
- テーマに追随すべき値に、16 進数や
rgb()を直に書かないこと。 - フォールバックは切り替わるトークンを指すこと。
var(--ran-color-text, var(--ran-gray-1000))であって、var(--ran-color-text, #171717)ではありません。ライト専用のリテラルはダークモードで消えます。 - フォールバックは存在するトークンを指すこと。宣言されていないプロパティへの
var()は何にも解決されず、宣言まるごとが捨てられ、要素は継承したものをそのまま保ちます。たいていは_ほぼ_正しく見えるのが厄介です。(--ran-color-errorは存在しません。--ran-color-dangerです。)
余白とリズム
すべての余白は9 段階のスケールから取り、距離そのものに意味を持たせてください。
- グループ内の要素どうしは 8px。
- グループとグループのあいだは 16px。
- セクションとセクションのあいだは 32〜40px。
20px や 28px を発明しないでください。ページのリズムを生むのは限られた選択肢のほうで、スケールから外れた余白ひとつがそれを壊します。領域をまたいだ共通の背骨(揃った端、ベースライン、カラム)を保ち、揃っているかは目ではなく描画されたピクセルで確かめてください。
文字の選び方
そのテキストがどんな役割を担うか(見出し、ラベル、本文、ボタン、等幅)を問えば、フォント・大きさ・太さ・行間はすべてタイプスケールから決まります。個別に生の px を選ばないでください。
役割は道具であって法律ではありません。本当に一回きりの装飾的なテキスト(ジェスチャーの閃光のようなオーバーレイ、アクティブなリンクの太さの微調整)は、いちばん近い役割に押し込むより、自前のコンポーネントトークンを持たせたほうが良い結果になります。
奥行き:影と重なり
影の段階は、その要素が何であるかで選び(流れの中にある面、浮いているオーバーレイ、行く手をふさぐダイアログ)、それが実際に知覚できることを確かめてください。見えない影は奥行きの手がかりになりませんし、オーバーレイがカードの段階に落ちてしまうと、ページに貼り付いているように見えます。
あなた自身の外枠に ranui のオーバーレイを埋め込むとき。 z-index のはしごが 1000 から始まるのは、まさに普通のページの外枠を越えるためです。だからポータルされたオーバーレイに、あなたの手助けは要りません。ただし自分のシャドウ DOM の中に留まる position: fixed のオーバーレイ(r-modal のダイアログ)は、いちばん近い祖先のスタッキングコンテキストまでしか抜け出せません。ですから埋め込んだ内容を、スタッキングコンテキストを作るもの(isolation、opacity < 1、transform、filter、will-change)で包んだ場合は、その包みのスタッキングレベルを上げて、ダイアログがふたたびその上に重なるようにする必要があります。引き上げるのは、オーバーレイが_実際に_開いているあいだだけに限ってください。
.embed {
isolation: isolate; /* 安上がり:自前の z-index を持たないので、何も引き上げられません */
}
/* 本物のオーバーレイが開いているあいだだけ引き上げる。決して「念のため」ではなく */
.embed:has(r-modal[open]),
.embed:has(r-modal[closing]) {
position: relative;
z-index: 100;
}包みに一律の z-index を付けると、その中の_すべて_(まったく動かない内容まで)が、スクロールしているあいだずっと、あなたの固定ヘッダーより上に持ち上がります。このバグは、ほかならぬこのサイトで一度出荷されました。open だけでなく closing にも一致させてください。マスクは open が外れたあとも、トランジションのぶんだけ描画を続けます。
モーション
変化が大きいほど時間を長く。その閾値より小さければ、そもそもアニメーションさせないでください。ホバーとアクティブのフィードバックは約 150ms、メニューは約 200ms、ダイアログは約 300ms、すでに明らかな変化は 0ms です。prefers-reduced-motion を尊重してください。
パレットのプロパティを決してトランジションさせないこと。 CSS は色が_なぜ_変わったのかを知りません。だから background-color、color、border-color、box-shadow、fill、stroke に付けた transition は、テーマが切り替わったときにも発火し、ページのほかが切り替わり終えたあとも、各要素がそれぞれのペースでフェードします。代わりに動きのプロパティ(transform、opacity、ジオメトリ)をアニメーションさせてください。transition: all と transition: 0.2s のような素のショートハンドは_すべて_を意味し、パレットのプロパティも含みます。どちらも ranui 自身のスタイルでは禁止されており、あなたのスタイルでも悪い考えです。
状態と文言
たどり着ける状態はすべてデザインの一部です。ホバー、アクティブ、フォーカス、無効、読み込み中、空、エラー。それらをはしごに対応させます。ホバー → bg-hover / border-hover、アクティブ → bg-active、無効 → text-disabled と不透明度の低下、フォーカス → フォーカスリング。
操作できないものが操作できるように見えてはいけません。r-card がホバーに反応するのは hoverable 属性が付いたときだけです。クリックしないカードには付けないでください。
文言もシステムの一部です。
- ボタンは動作と対象を書きます。✅「メンバーを削除」❌「削除」「OK」。
- エラーは何が起きたかを述べ、次にどう直すかを述べます。✅「ビルドに失敗しました。バンドルがサイズ上限を超えています。減らすか、上限を引き上げてください。」❌「操作に失敗しました。もう一度お試しください。」
- 確認とトーストは、成功ではなく変化を述べます。✅「プロジェクトを削除しました」❌「削除に成功しました」(トーストが出ていること自体が、すでに成功を語っています)。
- 文脈に冗長さを取り除かせてください。「プロジェクトを削除」というタイトルのダイアログに、「プロジェクトを永久に、完全に削除する」というボタンは要りません。
アクセシビリティ
- テキストと背景のコントラストは WCAG AA を満たすこと。
- 状態を色だけで示さないこと。 アイコン、ラベル、文字と組み合わせます。
- すべての操作できる要素は見えるフォーカスリングを保つこと(
--ran-focus-ring、またはoutline: 2px solid var(--ran-color-primary); outline-offset: 2px)。見た目を整えるために消さないでください。 - すべてキーボードで到達できること。 マウス専用のものを作らないこと。
prefers-reduced-motionとprefers-color-schemeを尊重すること。
マウスとタッチ、狭い画面と広い画面
入力方式もビューポートも、どちらかが二の次ということはありません。
- ドラッグ、スライダー、ジェスチャーの操作は Pointer Events(
pointerdown/pointermove/pointerup/pointercancel)を使い、mouse*だけで済ませないこと。あわせて、ドラッグする面そのものにtouch-action: noneを付けます。ポインターのハンドラーを伴わずにtouch-action: noneだけ宣言した CSS は、無害な空振りではなく壊れたコントロールです。 - ホバーでしか現れない手がかりには、タップの代替が要ります。
r-selectやr-popoverのtrigger="hover"はタッチデバイスではクリックに落ちます。あなたが作るものも同じであるべきです。 - ブレークポイントを発明するより、ビューポート相対の寸法(
%、min()、max()、clamp()、vw/vh。たとえばmin(560px, calc(100vw - 32px)))を選んでください。ranui に共有のブレークポイントトークンはないので、決め打ちのブレークポイントはすべて、誰かが保守しなければならない一回きりの数字になります。 - モバイルで、それをする唯一の方法を隠さないこと。
display: noneではなく回り込みで対応します。 - 測った位置が正しいのは、次のリフローまでです。
getBoundingClientRect()から導いたものは、リサイズで、コンテナのリフローで、そして(ポータルされたパネルなら)スクロールで古くなります。最初に測るきっかけとなった操作のときだけでなく、それらのイベントでも測り直してください。狭い幅でページを読み込むのは初期レイアウトを試すことであって、_そこへリサイズする_ことを試してはいません。この種のバグが実際に現れるのは後者です。
ライブラリが機械的に強制していること
これらのルールのうち 9 つは pnpm -F ranui verify:design が確かめており、CI が ranui 自身のソースに対して走らせています。ダークで安全でない色のフォールバック、生の色リテラル、余白スケール、寸法スケール、マウス専用のドラッグループ、hidden を壊す :host の display 規則、宣言されていないトークンを指すフォールバック、自分のシャドウツリーを query するコンポーネント、コンストラクターの外で作られたシャドウツリー。既知の違反はベースラインのファイルで固定されているので、新しい違反を増やすこともできず、直したものが黙って元に戻ることもありません。
このゲートが守るのはライブラリであってあなたのアプリではありません。ただ、それが捕まえる失敗の型(存在しないトークンを指すフォールバック、ライトモードでしか成立しない色)は、まさにレビューでは問題なく見えるものです。だから同じルールは、あなた自身の CSS にも当てはめる価値があります。
UI を出す前のチェックリスト
- [ ] 主たるタスクと主たるアクションが疑いようがない。
- [ ] ライトとダーク、狭い幅と広い幅で動く。
- [ ] マウスとタッチで動く。ホバーで開くものにはタップの代替がある。
- [ ] すべての状態を試した:ホバー、アクティブ、フォーカス、無効、読み込み中、空、エラー。
- [ ] キーボードとフォーカスを確認した。フォーカスはどこでも見える。
- [ ] 端の場合:長いテキスト、大きな数値、両方のロケール。
- [ ] 余白はスケールから、文字は役割で、色はセマンティックトークンから。
- [ ]
transitionにパレットのプロパティがない。transition: allもない。 - [ ] 文言が対象を名指ししている。色だけで状態を示しているものがない。