css-architecture について
Rails アプリ向けの CSS アーキテクチャを案内するスキルです。デザイントークン、コンポーネント単位のスタイル、カスケードレイヤーを軸に、モダン CSS によるダークモードにも対応します。
主な機能
- 01コンポーネント単位のスタイルシート構成
- 02デザイントークンによる値の統一
- 03@layer によるカスケード制御
- 04light-dark() によるダークモード対応
- 05サードパーティ CSS の統合ガイド
活用シーン
- 01Rails アプリの CSS 新設や再編成
- 02再利用可能なコンポーネントスタイルの作成
- 03ダークモードとデザイントークンの導入
- 04カスケードや詳細度の問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill css-architectureインストール後、Claude Code で/css-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/components.mddark-mode.mddesign-tokens.mdlayers.mdresponsive.mdutilities.md詳細
Rails CSS アーキテクチャのエキスパート
モダンな CSS 機能(カスタムプロパティ、@layer、light-dark()、セマンティックデザイントークン)を使って、Rails アプリケーション向けの保守性と拡張性に優れた CSS を作成します。フレームワークへの依存はありません。
設計思想
基本原則:
- コンポーネントは自身のスタイルを管理する — コンポーネントごとに CSS ファイルを 1 つ用意し、ページのスタイルシートからコンポーネントのスタイルを設定しません
- マジックナンバーよりデザイントークンを使う — すべての値は
_global.cssのカスタムプロパティから取得します - レイヤーでカスケードを制御する — 詳細度のハックや
!importantの代わりに@layerを使います - ダークモードは追加作業不要です —
color-schemeとlight-dark()を使えば、手間をかけずにダークモードを利用できます - ユーティリティは主役ではなく、味付けです — 単発の調整に控えめに使い、主なスタイリング手段にはしません
アーキテクチャのピラミッド:
/\ Utilities (few — spacing/text tweaks)
/ \
/____\ Components (many — reusable UI pieces)
/ \
/________\ Base (element defaults, resets)
/ \
/____________\ Tokens (_global.css — design system foundation)
このスキルを使う場面
- Rails アプリで CSS ファイル構成を作成または再編成するとき
- 新しいコンポーネントのスタイルシートを作成するとき
- ダークモードへの対応を実装するとき
- デザイントークンを定義または拡張するとき
- カスケードや詳細度の問題をデバッグするとき
- Tailwind やその他のフレームワークからバニラ CSS に移行するとき
- CSS レイヤーを設定するとき
- ユーティリティクラスを作成するとき
- サードパーティ製 CSS ライブラリを統合するとき
手順
ステップ 1:ファイル構成を把握する
このアーキテクチャを使う Rails アプリは、すべて次の構成になっています。
app/assets/stylesheets/
├── _global.css # Design tokens: colors, spacing, typography, radii
├── application.css # Entry point — may just import everything
├── reset.css # CSS reset / normalization
├── base.css # Element defaults (no classes)
├── utilities.css # Single-purpose utility classes
└── components/
├── app-layout.css # Main layout shell
├── forms.css # Form elements + buttons
├── cards.css # Card component
├── badges.css # Badge variants
├── alerts.css # Alerts/flash messages
├── tables.css # Table styles
└── [feature].css # Feature-specific components
必ず、まず既存の構成を確認してください:
# See what exists
find app/assets/stylesheets -name "*.css" | sort
# Check for existing tokens
cat app/assets/stylesheets/_global.css
# Check layer declarations
rg "@layer" app/assets/stylesheets/
# Check for existing components
ls app/assets/stylesheets/components/
既存のプロジェクト規約に合わせてください — 理想的なパターンより一貫性が重要です。
ステップ 2:CSS レイヤーを設定する
レイヤーは _global.css(または application.css)の先頭で一度だけ宣言します。
/* _global.css — FIRST LINE */
@layer reset, base, components, utilities;
レイヤーの優先順位(低い → 高い):
- reset — ブラウザーの正規化
- base — 要素のデフォルトスタイル(
h1、a、input) - components — UI コンポーネント(
.card、.btn、.badge) - utilities — 上書き用ヘルパー(
.mt-4、.hidden)— 常に最優先
すべての CSS ルールをレイヤー内に記述します。
/* base.css */
@layer base {
body { font-family: var(--font-sans); color: var(--color-ink); }
a { color: var(--color-link); }
}
/* components/cards.css */
@layer components {
.card { background: var(--color-surface); border: 1px solid var(--color-border); }
}
/* utilities.css */
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
レイヤーが重要な理由: @layer がないと、ネストや !important、読み込み順のハックを使って詳細度と格闘することになります。レイヤーを使えば、こうした問題をすべて解消できます。utilities レイヤー内のユーティリティは、詳細度にかかわらず、あらゆるコンポーネントルールより優先されます。
ステップ 3:デザイントークンを定義する
すべてのデザイン値は、:root のカスタムプロパティとして _global.css に記述します。
:root {
/* Enable dark mode detection */
color-scheme: light dark;
/* === Raw Palette (OKLCH for perceptual uniformity) === */
--color-zinc-50: oklch(0.985 0 0);
--color-zinc-100: oklch(0.967 0.001 286.375);
--color-zinc-200: oklch(0.92 0.004 286.32);
--color-zinc-400: oklch(0.707 0.022 261);
--color-zinc-500: oklch(0.552 0.016 285.938);
--color-zinc-700: oklch(0.37 0.013 285.805);
--color-zinc-800: oklch(0.274 0.006 286.033);
--color-zinc-900: oklch(0.21 0.006 285.885);
--color-zinc-950: oklch(0.141 0.005 285.823);
/* === Semantic Color Tokens === */
/* Surfaces */
--color-canvas: light-dark(var(--color-zinc-50), var(--color-zinc-900));
--color-surface: light-dark(white, var(--color-zinc-800));
--color-surface-raised: light-dark(white, var(--color-zinc-700));
--color-surface-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
/* Text */
--color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
--color-ink-muted: light-dark(var(--color-zinc-500), var(--color-zinc-400));
--color-ink-subtle: light-dark(var(--color-zinc-400), var(--color-zinc-500));
/* Borders */
--color-border: light-dark(var(--color-zinc-200), var(--color-zinc-700));
--color-border-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
--color-border-strong: light-dark(var(--color-zinc-400), var(--color-zinc-600));
/* Interactive */
--color-primary: light-dark(var(--color-blue-600), var(--color-blue-500));
--color-primary-hover: light-dark(var(--color-blue-700), var(--color-blue-400));
--color-link: light-dark(var(--color-blue-600), var(--color-blue-400));
/* State */
--color-positive: light-dark(var(--color-green-600), var(--color-green-500));
--color-positive-canvas: light-dark(var(--color-green-50), var(--color-green-950));
--color-negative: light-dark(var(--color-red-600), var(--color-red-500));
--color-negative-canvas: light-dark(var(--color-red-50), var(--color-red-950));
/* === Spacing Scale === */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
/* === Typography === */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, "SF Mono", monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* === Radii === */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-full: 9999px;
/* === Transitions === */
--duration-fast: 150ms;
--duration-normal: 250ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
ルール:コンポーネントで生の値を使うのは厳禁です。必ずトークンを参照してください。
/* GOOD */
.card { padding: var(--space-6); background: var(--color-surface); }
/* BAD — hardcoded values */
.card { padding: 1.5rem; background: white; }
ステップ 4:コンポーネントのスタイルを記述する
各コンポーネントには components/ 内に専用のファイルを用意します。コンポーネントではトークンのみを使います。
コンポーネントの構成:
/* components/cards.css */
@layer components {
/* Base */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
}
/* Structural subcomponents */
.card-header { padding-bottom: var(--space-4); border-bottom: 1px solid var(--color-border-muted); }
.card-body { padding-top: var(--space-4); }
.card-footer { padding-top: var(--space-4); border-top: 1px solid var(--color-border-muted); }
/* Variants */
.card-sm { padding: var(--space-4); }
.card-lg { padding: var(--space-8); }
/* States */
.card-interactive { transition: border-color var(--duration-fast) var(--ease-out); }
.card-interactive:hover { border-color: var(--color-border-strong); }
}
命名規則:
- 基本形:
.card、.btn、.badge - サブコンポーネント:
.card-header、.card-body - バリエーション:
.card-sm、.btn-primary - 状態:
.is-loading、.is-active
新しいコンポーネントファイルを作成する場合:
- パターンを 3 回以上使う場合
- 関連するサブクラス(
.foo、.foo-header、.foo-item)がある場合 - バリエーション(
.foo-primary、.foo-small)がある場合
ページのスタイルシートに記述する場合:
- ページ固有のスタイルである場合(1 回のみ使用)
- シンプルなレイアウトである場合(グリッド、フレックスコンテナー)
ステップ 5:light-dark() でダークモードに対応する
トークンシステムに従えば、ダークモードは自動的に適用されます。手順は次のとおりです。
:rootにcolor-scheme: light darkを設定する — ブラウザーが OS の設定に従いますlight-dark()でセマンティックトークンを定義する — 1 つ目の値はライトモード、2 つ目の値はダークモードです- コンポーネントからセマンティックトークンを参照する — スタイルは自動的に切り替わります
:root {
color-scheme: light dark;
--color-surface: light-dark(white, var(--color-zinc-800));
--color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
}
/* This component supports dark mode with ZERO extra work */
.card {
background: var(--color-surface);
color: var(--color-ink);
}
手動切り替え(任意):
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }
function toggleTheme() {
const current = document.documentElement.dataset.theme;
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
}
フラッシュを防ぐには、これを <head> に記述します:
<script>
const theme = localStorage.getItem('theme');
if (theme) document.documentElement.dataset.theme = theme;
</script>
ダークモードのルール:
- ダークモードでは影が機能しないため、境界線を優先します
- 控えめな色付き背景には
color-mix()を使います - 状態色には、前景色とキャンバス色の両方のバリエーションが必要です
- 生の白や黒は使わず、必ずセマンティックトークンを使います
ステップ 6:ユーティリティクラスを記述する
ユーティリティは @layer utilities 内の utilities.css に記述します。各ユーティリティは単一の用途に絞り、変更しないものとします。
@layer utilities {
/* Spacing */
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mb-4 { margin-bottom: var(--space-4); }
.p-4 { padding: var(--space-4); }
.gap-4 { gap: var(--space-4); }
/* Text */
.text-sm { font-size: var(--text-sm); }
.text-muted { color: var(--color-ink-muted); }
.font-medium { font-weight: var(--font-medium); }
/* Layout */
.flex { display: flex; }
.grid { display: grid; }
.hidden { display: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}
単発の調整にはユーティリティを使います:
<%# Good — utility for spacing adjustment %>
<div class="card mt-6">
<%# Bad — utilities replacing component styles %>
<div class="p-6 bg-white border rounded-lg shadow">
ステップ 7:ページのスタイルシートは薄く保つ
ページ固有のスタイルは最小限にし、レイアウトだけを指定します。
/* pages/dashboard.css */
@layer components {
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-6);
}
}
ページは既存の要素を組み合わせます。
<div class="dashboard-grid">
<div class="card">...</div>
<div class="card">...</div>
<div class="card mt-4">...</div> <%# Component + utility %>
</div>
ページのスタイルシートからコンポーネントのスタイルを指定しないでください。
/* BAD — page overriding component */
.dashboard .card { padding: 2rem; }
/* GOOD — create a variant in the component file */
.card-lg { padding: var(--space-8); }
ステップ 8:サードパーティ製 CSS を統合する
外部ライブラリを統合する場合は、その変数をトークンに割り当てます。
:root {
--lexxy-color-ink: var(--color-ink);
--lexxy-color-canvas: var(--color-surface);
--lexxy-color-selected: light-dark(oklch(0.92 0.026 254), oklch(0.3 0.05 254));
}
読み込み順序が重要です — ライブラリの CSS は上書き設定より前に読み込みます。
<%= stylesheet_link_tag "lexxy" %>
<%= stylesheet_link_tag :app %>
アンチパターン
| 避けること | 代わりに行うこと |
|---|---|
色をハードコードする(white、#333) | セマンティックトークンを使う(var(--color-surface)) |
!important | カスケードの制御に @layer を使う |
深くネストする(.page .section .card .title) | フラットで具体的なクラスを使う(.card-title) |
汎用的な名前(.container、.title) | プレフィックス付きの名前を使う(.page-container、.card-title) |
| ページ CSS からコンポーネントのスタイルを指定する | コンポーネントのバリアントを作成する |
| ERB 内でインラインスタイルを使う | ユーティリティクラスまたはコンポーネントクラスを使う |
ダークモードの浮き上がり表現に box-shadow を使う | ボーダーを使う |
マジックナンバーを使う(padding: 23px) | スペーシングトークンを使う(var(--space-6)) |
| ダークモードにメディアクエリを使う | color-scheme と light-dark() を使う |
| ビューでフレームワーク固有のクラスを使う | 独自のセマンティックなクラスを使う |
クイックリファレンス
命名規則
.component → .card, .btn, .badge
.component-sub → .card-header, .card-body
.component-variant → .card-sm, .btn-primary
.component-state → .card-interactive (or .is-loading for global states)
.page-element → .dashboard-grid, .settings-sidebar
レスポンシブパターン
/* Mobile-first with container queries where possible */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-6);
}
}
ブラウザーのサポート
light-dark() は Chrome 123 以降、Firefox 120 以降、Safari 17.5 以降が必要です。
古いブラウザー向けのフォールバック:
@media (prefers-color-scheme: dark) {
:root {
--color-surface: var(--color-zinc-800);
--color-ink: var(--color-zinc-50);
}
}
新しいコンポーネントのチェックリスト
-
components/[name].cssを作成した - すべてのルールを
@layer components { }で囲んだ - すべての値がデザイントークンを参照している(マジックナンバーなし)
- セマンティックなカラートークンを使っている(生のパレット値ではない)
- クラス名が具体的で、衝突しない
- サブコンポーネントが
.component-subの命名規則に従っている - ダークモードが自動で機能することを確認した
- 必要に応じて
application.cssにインポートした
デザイントークンのチェックリスト
- 新しいトークンを
_global.cssの:root内に追加した - カラートークンで両方のモードに
light-dark()を使っている - 生のパレット値に OKLCH を使っている
- スペーシングが既存のスケールに沿っている
- トークン名がセマンティックである(
--color-whiteではなく--color-surface)
詳細なパターンと例については、references/ ディレクトリを参照してください:
references/design-tokens.md— カラーパレット全体(OKLCH)、スペーシング、タイポグラフィ、命名規則references/components.md— ボタン、カード、バッジ、アラート、フォーム、テーブルのパターンと Rails の統合references/dark-mode.md—light-dark()の詳細、手動切り替え、シャドウ、color-mix()references/responsive.md— モバイルファーストのブレークポイント、コンテナークエリ、流動的なタイポグラフィ、レイアウトパターンreferences/layers.md—@layerによるカスケード制御、アンチパターン、サードパーティ製 CSSreferences/utilities.md— 推奨ユーティリティクラスの一覧