mcpField
C
css-architecture

css-architecture

github.com/thinkoodleデザイン・UI7176 回インストール8 週間前に更新
リポジトリ

css-architecture について

Rails アプリ向けの CSS アーキテクチャを案内するスキルです。デザイントークン、コンポーネント単位のスタイル、カスケードレイヤーを軸に、モダン CSS によるダークモードにも対応します。

主な機能

  1. 01コンポーネント単位のスタイルシート構成
  2. 02デザイントークンによる値の統一
  3. 03@layer によるカスケード制御
  4. 04light-dark() によるダークモード対応
  5. 05サードパーティ CSS の統合ガイド

活用シーン

  1. 01Rails アプリの CSS 新設や再編成
  2. 02再利用可能なコンポーネントスタイルの作成
  3. 03ダークモードとデザイントークンの導入
  4. 04カスケードや詳細度の問題の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill css-architecture

インストール後、Claude Code で/css-architectureを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
components.md
dark-mode.md
design-tokens.md
layers.md
responsive.md
utilities.md

詳細

Rails CSS アーキテクチャのエキスパート

モダンな CSS 機能(カスタムプロパティ、@layer、light-dark()、セマンティックデザイントークン)を使って、Rails アプリケーション向けの保守性と拡張性に優れた CSS を作成します。フレームワークへの依存はありません。

設計思想

基本原則:

  1. コンポーネントは自身のスタイルを管理する — コンポーネントごとに CSS ファイルを 1 つ用意し、ページのスタイルシートからコンポーネントのスタイルを設定しません
  2. マジックナンバーよりデザイントークンを使う — すべての値は _global.css のカスタムプロパティから取得します
  3. レイヤーでカスケードを制御する — 詳細度のハックや !important の代わりに @layer を使います
  4. ダークモードは追加作業不要です — color-scheme と light-dark() を使えば、手間をかけずにダークモードを利用できます
  5. ユーティリティは主役ではなく、味付けです — 単発の調整に控えめに使い、主なスタイリング手段にはしません

アーキテクチャのピラミッド:

      /\       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;

レイヤーの優先順位(低い → 高い):

  1. reset — ブラウザーの正規化
  2. base — 要素のデフォルトスタイル(h1、a、input)
  3. components — UI コンポーネント(.card、.btn、.badge)
  4. 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() でダークモードに対応する

トークンシステムに従えば、ダークモードは自動的に適用されます。手順は次のとおりです。

  1. :root に color-scheme: light dark を設定する — ブラウザーが OS の設定に従います
  2. light-dark() でセマンティックトークンを定義する — 1 つ目の値はライトモード、2 つ目の値はダークモードです
  3. コンポーネントからセマンティックトークンを参照する — スタイルは自動的に切り替わります
: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 によるカスケード制御、アンチパターン、サードパーティ製 CSS
  • references/utilities.md — 推奨ユーティリティクラスの一覧

コメント