rails-components について
rails-componentsは、CSSクラス、partial、helper、ViewComponentを使った再利用可能なRails UIコンポーネントの構築を導くスキルです。適切で簡潔な実装方法を選び、構造や規約を統一するのに役立ちます。
主な機能
- 01CSSからViewComponentまでの選択ガイド
- 02再利用可能なpartialとhelperのパターン
- 03コンポーネントの命名・ファイル規約
- 04スロットとバリアントの実装指針
- 05コンポーネントギャラリーの設定
活用シーン
- 01新しいUIコンポーネントの実装方法の選択
- 02重複したRailsマークアップのリファクタリング
- 03共有コンポーネントのスタイルとテンプレートの整理
- 04開発用コンポーネントギャラリーの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill rails-componentsインストール後、Claude Code で/rails-componentsを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
Rails コンポーネントのパターン
Rails のプリミティブであるパーシャル、CSS クラス、ヘルパーを使って、再利用可能で自己完結型かつ組み合わせ可能な UI コンポーネントを構築します。
考え方
基本原則:
- まず CSS クラスから始める — クラスで済む場合はパーシャルを使わないでください
- 自己完結型 — 各コンポーネントは、マークアップとスタイルを同じ場所で管理します
- 組み合わせ可能 — コンポーネントは自然に入れ子にしたり組み合わせたりできます
- 一貫性 — 同じコンポーネントなら、同じマークアップと表示になります
- 段階的に複雑化する — 必要になるまでは、CSS → パーシャル → ヘルパーの順に進みます
コンポーネントの段階:
ViewComponent (gem) ← Complex: encapsulated Ruby + template + tests
Helper method ← Frequent: shorthand for common partials
Partial with locals ← Structured: logic, slots, complex markup
CSS classes only ← Simple: single element, few variants
必ず一番下から始めてください。不便を感じたときにだけ上の段階へ進みます。
このスキルを使う場面
- 再利用可能な新しい UI コンポーネント(ボタン、カード、モーダルなど)の構築
- CSS クラス、パーシャル、ヘルパー、ViewComponent のどれを使うか判断するとき
- 空状態、アラート、バッジ、データテーブルの作成
- 繰り返されるマークアップを共通コンポーネントにリファクタリング
- 開発用コンポーネントギャラリーの設定
- バリエーションのパターン(サイズ、色、状態)の実装
- コンポーネントのファイルやスタイルの整理
判断の基準
CSS クラスを使う場合:
- コンポーネントが単一の要素、または単純なラッパー(
<button>、<span>、<div>)である - バリエーションが純粋に見た目に関するもの(色、サイズ、余白)である
- 条件分岐が不要である
- 内側のテキスト以外にコンテンツスロットがない
例: ボタン、バッジ、シンプルなカード、フォーム入力、区切り線
パーシャルを使う場合:
- 条件付きのセクションがある(引数に応じて表示/非表示を切り替える)
- 名前付きのコンテンツ領域が複数ある(ヘッダー、本文、フッター)
- マークアップの構造が単純ではない(要素が 5 個以上)
- デフォルト値や計算された属性が必要である
例: 空状態、モーダル、複雑なカード、データテーブル、アイコン付きアラート
ヘルパーを使う場合:
- パーシャルが非常に頻繁に使われる(呼び出し箇所が 10 か所以上)
- より簡潔な Ruby API を使いたい:
ui_badge("Active", variant: :success) - ERB の render 構文が重く感じられるシンプルなコンポーネント
- 複数の要素を Ruby で組み合わせたい
例: バッジ、ステータスインジケーター、アイコンボタン、パンくずリスト
ViewComponent を使う場合:
- コンポーネント自体の単体テストが必要である
- ビューに置くべきではない複雑な状態ロジックがある
- チームの規模が大きく、コンポーネントに厳密なインターフェースが必要である
- 数十個のコンポーネントを備えたデザインシステムを構築する
ほとんどの Rails アプリでは ViewComponent を使わないでください。 パーシャルとヘルパーでニーズの 90% をカバーできます。
手順
ステップ 1:既存のコンポーネントを確認する
必ず最初に既存のパターンを探してください:
# Find existing component partials
find app/views -path "*/components/*" -o -path "*/_component*" | head -20
# Find component CSS
find app/assets -name "*component*" -o -path "*/components/*" | head -20
# Find component helpers
rg "def ui_|def component_" app/helpers/
# Check for ViewComponent usage
ls app/components/ 2>/dev/null
rg "ViewComponent" Gemfile
既存プロジェクトの規約に合わせてください。 自分の好みより一貫性を優先します。
ステップ 2:適切な方法を選ぶ
上記の判断基準を参考にしてください。次の点を確認します:
- CSS クラスだけで実現できますか? → その方法を使います。
- 条件分岐やスロットが必要ですか? → パーシャルを使います。
- 同じパターンで 10 か所以上から呼び出されますか? → ヘルパーを追加します。
- 独立したテストと複雑な状態が必要ですか? → ViewComponent を使います。
ステップ 3:ファイル構成
app/views/components/ # Shared component partials
├── _modal.html.erb
├── _empty_state.html.erb
├── _card.html.erb
├── _alert.html.erb
└── _data_table.html.erb
app/assets/stylesheets/components/ # One CSS file per component
├── buttons.css
├── badges.css
├── cards.css
├── modals.css
├── empty-states.css
├── alerts.css
└── tables.css
app/helpers/
└── component_helper.rb # Helper shortcuts for common components
命名規則:
- パーシャル:
app/views/components/内に_snake_case.html.erb - CSS ファイル:
app/assets/stylesheets/components/内にkebab-case.css - ヘルパー:
ComponentHelper内でui_component_nameをプレフィックスとして使う - CSS クラス:
.component-name、.component-name-element、.component-name--variant
ステップ 4:CSS のみでコンポーネントを構築する
シンプルなコンポーネントでは、CSS クラスを定義してテンプレートで直接使います:
/* app/assets/stylesheets/components/buttons.css */
@layer components {
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-primary { background: var(--color-primary); color: white; }
.btn-secondary { background: var(--color-surface); border: 1px solid var(--color-border); }
.btn-danger { background: var(--color-danger); color: white; }
.btn-ghost { background: transparent; }
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
.btn-icon { padding: 0.5rem; }
}
<%# Usage — no partial needed %>
<button class="btn btn-primary">Save</button>
<button class="btn btn-danger btn-sm">Delete</button>
<%= link_to "View", post_path(@post), class: "btn btn-secondary" %>
ステップ 5:パーシャルベースのコンポーネントを構築する
すべてのパーシャルには、インターフェースを記述するコメントブロックを必ず付けてください:
<%# app/views/components/_empty_state.html.erb %>
<%#
Empty State Component
Arguments:
icon: (String) Icon name — required
title: (String) Main heading — required
description: (String, optional) Supporting text
class: (String, optional) Additional CSS classes
Block: Optional action buttons
%>
<div class="empty-state <%= local_assigns[:class] %>">
<div class="empty-state-icon">
<%= lucide_icon icon, size: 48 %>
</div>
<h3 class="empty-state-title"><%= title %></h3>
<% if local_assigns[:description] %>
<p class="empty-state-description"><%= description %></p>
<% end %>
<% if block_given? %>
<div class="empty-state-actions">
<%= yield %>
</div>
<% end %>
</div>
パーシャルに関する主なルール:
- オプション引数の確認には
local_assigns[:key]を使います —defined?やif keyは使いません - 引数にフォールバック値が必要な場合は、デフォルト値に
||=を使います - ブロックが渡されたかどうかの確認には
block_given?を使います - 主要なコンテンツスロットには
yieldを使います - 追加の名前付きスロットには
captureを使います
ステップ 6:capture でスロットを実装する
コンテンツ領域が複数あるコンポーネントの場合:
<%# app/views/components/_card.html.erb %>
<%#
Card Component
Arguments:
header: (String|HTML, optional) Header content — use capture for complex HTML
footer: (String|HTML, optional) Footer content — use capture for complex HTML
class: (String, optional) Additional CSS classes
variant: (Symbol, optional) Card variant — :default, :stat, :interactive
Block: Card body content (required)
%>
<%
variant ||= :default
css_class = ["card", ("card-#{variant}" unless variant == :default), local_assigns[:class]].compact.join(" ")
%>
<div class="<%= css_class %>">
<% if local_assigns[:header] %>
<div class="card-header"><%= header %></div>
<% end %>
<div class="card-body">
<%= yield %>
</div>
<% if local_assigns[:footer] %>
<div class="card-footer"><%= footer %></div>
<% end %>
</div>
呼び出し側では、リッチなスロットコンテンツに capture を使います:
<%= render "components/card",
header: capture { tag.h3("Settings", class: "card-title") },
footer: capture {
link_to("Cancel", "#", class: "btn btn-secondary") +
link_to("Save", "#", class: "btn btn-primary")
} do %>
<p>Card body content here.</p>
<% end %>
ステップ 7:ヘルパーショートカットを追加する
頻繁に使用するコンポーネントには、ヘルパーを作成します。
# app/helpers/component_helper.rb
module ComponentHelper
def ui_badge(text, variant: :muted)
tag.span(text, class: "badge badge-#{variant}")
end
def ui_empty_state(title:, icon: nil, description: nil, &block)
render("components/empty_state",
title: title,
icon: icon,
description: description,
&block)
end
def ui_alert(variant: :info, dismissible: false, &block)
render("components/alert",
variant: variant,
dismissible: dismissible,
&block)
end
end
次のように簡潔に記述できます:
<%= ui_badge "Active", variant: :success %>
<%= ui_badge "Draft" %>
<%= ui_empty_state(
icon: "inbox",
title: "No messages",
description: "Check back later") %>
ステップ 8:バリエーションを扱う
見た目のみを変更する場合は、CSS のバリエーションを使用します。
/* Size variants */
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
/* State variants */
.card-interactive:hover { border-color: var(--color-border-strong); }
.card-flush .card-body { padding: 0; }
マークアップが変わる場合は、引数の一部を変えたバリエーションを使用します。
<%# In the partial %>
<%
variant ||= :info
icon_name = { success: "check-circle", error: "alert-circle",
warning: "alert-triangle", info: "info" }[variant.to_sym]
%>
<div class="alert alert-<%= variant %>" role="alert">
<%= lucide_icon icon_name, size: 18 %>
<span class="alert-content"><%= yield %></span>
</div>
ステップ 9:コンポーネントギャラリーを作成する
開発に不可欠です。 誰でもすべてのコンポーネントを閲覧できるようにします。
# config/routes.rb
if Rails.env.development?
get "components", to: "components#index"
end
# app/controllers/components_controller.rb
class ComponentsController < ApplicationController
layout "application"
def index
end
end
<%# app/views/components/index.html.erb %>
<div style="max-width: 48rem; margin: 2rem auto; padding: 0 1rem;">
<h1>Component Gallery</h1>
<h2>Buttons</h2>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-sm">Small</button>
</div>
<h2>Badges</h2>
<div style="display: flex; gap: 0.5rem; margin-bottom: 2rem;">
<span class="badge badge-muted">Draft</span>
<span class="badge badge-success">Active</span>
<span class="badge badge-warning">Pending</span>
<span class="badge badge-danger">Failed</span>
</div>
<h2>Empty States</h2>
<%= render "components/empty_state",
icon: "inbox",
title: "No messages yet",
description: "When you receive messages, they'll appear here." do %>
<button class="btn btn-primary">Compose</button>
<% end %>
<h2>Alerts</h2>
<%= render "components/alert", variant: :info do %>Info message<% end %>
<%= render "components/alert", variant: :success do %>Success message<% end %>
<%= render "components/alert", variant: :warning do %>Warning message<% end %>
<%= render "components/alert", variant: :error do %>Error message<% end %>
</div>
アンチパターン
- 何にでもパーシャルを使う —
<span class="badge">にrender "components/badge"は不要です。CSS クラスで十分です。 defined?(variable)を使う — パーシャルでは常にlocal_assigns[:variable]を使います。defined?は信頼できません。- コンポーネント固有のスタイルにグローバルスタイルを使う — コンポーネントの CSS は
components/に配置します。application.cssを汚さないでください。 - 命名に一貫性がない — 規則を決め、それに従います。
_empty_state.html.erbを使い、_emptyState.html.erbは使いません。 - 巨大なコンポーネント — パーシャルが 10 個以上の引数を取る場合は、より小さなコンポーネントに分割します。
- ドキュメントがない — すべてのパーシャルに、引数を列挙したコメントブロックが必要です。
- HTML 文字列を引数として渡す —
capture { }ブロックまたはyieldを使い、生の HTML 文字列は使わないでください。 - ギャラリーページがない — すべてのコンポーネントを一か所で確認できないと、内容にばらつきが生じます。
CSS の規約
/* Use @layer for specificity management */
@layer components {
.component-name { /* base styles */ }
.component-name-element { /* child element */ }
.component-name--variant { /* BEM variant — OR use component-variant */ }
}
デザイントークンの使用:
.component {
color: var(--color-ink); /* Text colors */
background: var(--color-surface); /* Backgrounds */
border: 1px solid var(--color-border); /* Borders */
padding: var(--space-4); /* Spacing scale */
font-size: var(--text-sm); /* Type scale */
border-radius: var(--radius-md); /* Border radius */
}
プロジェクトでデザイントークン/CSS カスタムプロパティを使用している場合は、それらを使います。使用していない場合は、一貫性のある生の値を使います。
クイックリファレンス
local_assigns チートシート
<%# Check if argument was passed (even if nil) %>
<% if local_assigns.key?(:title) %>
<%# Check if argument was passed AND is truthy %>
<% if local_assigns[:title] %>
<%# Default value %>
<% variant = local_assigns[:variant] || :default %>
<%# OR %>
<% variant ||= :default %>
<%# Pass-through CSS classes %>
<div class="component <%= local_assigns[:class] %>">
レンダリング構文
<%# Simple — no block %>
<%= render "components/empty_state", icon: "inbox", title: "Empty" %>
<%# With block %>
<%= render "components/modal", title: "Confirm" do %>
<p>Are you sure?</p>
<% end %>
<%# With capture slots %>
<%= render "components/card",
header: capture { tag.h3("Title") },
footer: capture { tag.button("Save", class: "btn btn-primary") } do %>
<p>Body</p>
<% end %>
一般的なコンポーネントのテンプレート
完全なテンプレートについては、このスキルディレクトリ内の reference.md を参照してください。
- ボタン、バッジ(CSS のみ)
- カード、空状態、アラート(パーシャル)
- Stimulus を使ったモーダル(パーシャル+JS)
- データテーブル(パーシャル)
- 上記すべてのヘルパーメソッド