【Rails】ViewComponentを使ってshadcn/uiのボタンコンポーネントを再現する
.png&w=3840&q=75)
今回はUIコンポーネントライブラリであるshadcn/ui
のボタンコンポーネントをRails
のViewComponent
で再現してみます。
ViewComponent
についてはこちらの記事でも解説しています。
shadcn/uiを使ってみる
shadcn/uiではカスタマイズ可能な様々なコンポーネントが準備されています。
今回参考にするbuttonコンポーネントを実際に使ってみます。
Nextjsの環境構築
動作確認用の環境としてNextjs
のプロジェクトを作成します。Nodejs
がインストールされている前提で進めていきます。
バージョンはv20.10.0
を使用しています。
npx create-next-app@latest
※記事執筆時点ではNext.js
バージョン15.1.7
でプロジェクトが作成されます。
プロジェクトの設定について質問されるので回答していきます。
今回はshadcn/ui
を使ってみることのみが目的なので設定は適当で大丈夫ですが、一例を載せておきます。
質問 | 回答 |
---|---|
What is your project named? | shadcnui-test(任意のプロジェクト名) |
Would you like to use TypeScript? | Yes(TypeScriptを使用) |
Would you like to use ESLint? | Yes(ESLintを使用) |
Would you like to use Tailwind CSS? | Yes(Tailwind CSSを使用) |
Would you like your code inside a | No(srcディレクトリを使用しない) |
Would you like to use App Router? (recommended) | Yes(App Routerを使用) |
Would you like to use Turbopack for | Yes(Turbo packを使用) |
Would you like to customize the import alias (`@/*` by default)? | No(import時のエイリアスをカスタマイズしない) |
プロジェクトディレクトリに移動してプロジェクト立ち上げ、ブラウザでの確認ができればNextjs
の環境構築は完了です。
ディレクトリ移動
$ cd shadcnui-test
プロジェクト立ち上げ
$ npm run dev
http://localhost:3000
にアクセス

shadcn/uiインストール
shadcn/ui
をインストールします。
今回は-d
オプションを付けてデフォルトのスタイル設定でインストールします。
$ npx shadcn@latest init -d
npx shadcn@latest ~
を実行する際に以下のようなReactのバージョンによる依存関係の警告が出る場合があります。
今回は強制的に使用する(Use --force
)を選択します。
It looks like you are using React 19.
Some packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).
? How would you like to proceed? › - Use arrow-keys. Return to submit.
buttonコンポーネント追加
インストールが完了したら以下を実行してbuttonコンポーネントを追加します。
$ npx shadcn@latest add button
components/ui
配下にbutton.tsx
が作成されます。
表示確認
作成されたコンポーネントを使ってみます。
app/page.tsx
のreturn
の内容を全て削除して以下のように修正します。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div>
<Button>ボタン</Button>
</div>
);
}
トップページにアクセスするとボタンのスタイルが適応されていることが確認できます。

buttonコンポーネントにはvariant
, size
という引数を渡すことができ、スタイルをカスタマイズできます。
variantの例
コード
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="p-4 flex gap-4">
<Button>default</Button>
<Button variant="destructive">destructive</Button>
<Button variant="outline">outline</Button>
<Button variant="secondary">secondary</Button>
<Button variant="ghost">ghost</Button>
<Button variant="link">link</Button>
</div>
);
}
表示

※ghost
, link
はホバーした時のスタイルが異なります。
sizeの例
コード
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="p-4 flex gap-4">
<Button>default</Button>
<Button size="lg">lg</Button>
<Button size="sm">sm</Button>
<Button size="icon">icon</Button>
</div>
);
}
表示

また、ボタンを非活性にしたり、スタイルを追加することもできます。
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="p-4 flex gap-4">
<Button disabled>disabled button</Button>
<Button className="w-full">custom class button</Button>
</div>
);
}
表示

Rails・ViewComponentで再現
shadcn/uiのbuttonコンポーネントをRailsのViewComponentで再現していきます。
環境構築
バージョンは以下を使用しています。
$ rails -v
Rails 8.0.1
$ ruby -v
ruby 3.3.4
Railsのプロジェクトを作成します。
$ rails new button-component --javascript esbuild --css tailwind
オプションとして以下を指定しています。
JavaScript
のビルドにESBuild
を使用css
フレームワークのTailwind CSS
を使用
ViewComponent追加
今回はディレクトリ階層のカスタマイズなどを行うため、view_component-contribというGemを使用します。
READMEに記載してある対話型のジェネレータを実行します。
$ rails app:template LOCATION="https://railsbytes.com/script/zJosO5"
いくつか質問されるので回答していきます。
質問 | 回答(説明) |
---|---|
Where do you want to store your view components? | app/views/components(コンポーネントファイルの保存先ディレクトリを指定) |
Would you like to use dry-initializer in your component classes? | y(dry-initializerを使用する) |
Do you use Stimulus? | y(Stimulusを使用する) |
Do you use TailwindCSS? | y(Tailwind CSSを使用する) |
Would you like to create a custom generator for your setup? | y(カスタムジェネレーターを使用する) |
Which template processor do you use? | 1(ERBを使用する) |
Style Variantsを追加
view_component-contrib
で準備されているStyle Variantsを使用できるようにしておきます。
class ApplicationViewComponent < ViewComponentContrib::Base
extend Dry::Initializer
include ViewComponentContrib::StyleVariants # 追加
end
Style Variants
を使うことで以下のようなスタイルの定義が可能になります。
style do
base {
%w[
# 共通のスタイル
]
}
variants {
variant {
primary {
%w[
# 個別のスタイル
]
}
outline {
%w[
# 個別のスタイル
]
}
}
}
end
Style Variants
はTailwind Variants, CVA variantsというライブラリから影響を受けているようです。
The idea is to define variants schema in the component class and use it to compile the resulting list of CSS classes. (Inspired by Tailwind Variants and CVA variants).
shadcn/uiでは同じ用途でCVA variantsを使用しています。(buttonコンポーネントにおける該当箇所)
buttonコンポーネント作成
表示確認
generate
コマンドを実行してbutton
コンポーネントを生成します。
引数としてshadcn/ui
と同じようにsize
, variant
を渡せるようにしておきます。
$ rails g view_component Button size variant
クラス定義、ロジックの記述などを行うcomponent.rb
とレンダリングを行うためのcomponent.html.erb
ファイルの他にテスト用のファイルやプレビュー用のファイルも生成されます。
生成するファイルの設定はgenerator
の設定ファイルから変更できます。
テスト用、プレビュー用のファイルを生成しない場合は以下のように修正します。
# frozen_string_literal: true
# Based on https://github.com/github/view_component/blob/master/lib/rails/generators/component/component_generator.rb
class ViewComponentGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
class_option :skip_test, type: :boolean, default: true # テストファイルを生成しない
class_option :skip_system_test, type: :boolean, default: true # システムテストファイルを生成しない
class_option :skip_preview, type: :boolean, default: true # プレビューファイルを生成しない
# 省略
表示用のページを作成します。
コントローラー、viewファイル作成
$ rails g controller home index
ルーティング
Rails.application.routes.draw do
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
# Defines the root path route ("/")
root "home#index"
end
button
コンポーネントのsize
,variant
のデフォルト値を定義しておきます。
# frozen_string_literal: true
class Button::Component < ApplicationViewComponent
option :size, default: proc { :default}
option :variant, default: proc { :default}
end
デフォルトで記述されていたwith_collection_parameter :button
は今回は使わないので削除しています。
こちらの記述もgeneratorファイルを編集することで生成しないよう設定ができます。
# frozen_string_literal: true
class <%= class_name %>::Component < <%= parent_class %>
with_collection_parameter :<%= singular_name %> # 削除
<%- if initialize_signature -%>
<%= initialize_signature %>
<%- end -%>
end
ルートページで表示してみます。
<%= render(Button::Component.new) %>
component.html.erb
の内容が表示されることが確認できます。

helperの定義
Button::Component.new
のような記述を簡潔に書けるようにhelper
を定義します。
module ApplicationHelper
def component(name, *args, **kwargs, &block)
component = (name.to_s.split("/").map(&:camelize).join("::") + "::Component").constantize
render(component.new(*args, **kwargs), &block)
end
end
これでコンポーネントの呼び出しが簡潔に書けるようになりました。
<%= component(:button) %>
スタイルの定義
shadcn/ui
のカラーを参考にスタイルを定義します。
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 追加 */
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 5.9% 10%;
}
Tailwind CSSのクラスで使えるように定義、クラスファイル内で定義したTailwind CSSのクラスを適応できるようパスを追加します。
module.exports = {
content: [
'./app/views/**/*.html.erb',
'./app/views/components/**/*.rb', // 追加
'./app/helpers/**/*.rb',
'./app/assets/stylesheets/**/*.css',
'./app/javascript/**/*.js'
],
// 追加
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
}
}
}
}
buttonコンポーネントのclass定義
shadcn/ui
を参考にStyle Variants
の構文を利用してスタイルを定義します。
# frozen_string_literal: true
class Button::Component < ApplicationViewComponent
option :variant, default: proc { :default }
option :size, default: proc { :default }
style do
base {
%w[inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0]
}
variants {
variant {
default {
%w[bg-primary text-primary-foreground shadow hover:bg-primary/90]
}
outline {
%w[border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground]
}
destructive {
%w[bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90]
}
secondary {
%w[bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80]
}
ghost {
%(hover:bg-accent hover:text-accent-foreground)
}
link {
%(text-primary underline-offset-4 hover:underline)
}
}
size {
default {
%w[h-9 px-4 py-2]
}
sm {
%w[h-8 rounded-md px-3 text-xs]
}
lg {
%w[h-10 rounded-md px-8]
}
icon {
"size-9"
}
}
}
end
end
viewファイルにスタイルを適応させます。
<button class="<%= style(variant:, size:) %>">
<%= content %>
</button>
ルートページのviewファイルを修正します。
<%= component(:button) {"button"} %>
ルートページにアクセスするとshadcn/uiのようなボタンが表示されていることが確認できます。

他のパターンも表示しています。
<div class="p-4 flex gap-4">
<%= component(:button) {"button"} %>
<%= component(:button, variant: :outline) {"outline"} %>
<%= component(:button, variant: :destructive) {"destructive"} %>
<%= component(:button, variant: :secondary) {"secondary"} %>
<%= component(:button, variant: :ghost) {"ghost"} %>
<%= component(:button, variant: :link) {"link"} %>
</div>
<div class="p-4 flex gap-4">
<%= component(:button) {"button"} %>
<%= component(:button, size: :lg) {"lg"} %>
<%= component(:button, size: :sm) {"sm"} %>
<%= component(:button, size: :icon) {"icon"} %>
</div>
variant
, style
の値によってスタイルが変更できていることが確認できます。

スタイルを上書きできるようにする
最後にshadcn/ui
と同じようにクラスを追加してスタイルを上書きできるようにします。
tailwind_mergeというgem
を追加します。
gem "tailwind_merge"
bundle install
クラスをマージする関数を定義します。
class ApplicationViewComponent < ViewComponentContrib::Base
extend Dry::Initializer
include ViewComponentContrib::StyleVariants
## 追加
private
def tw_merge(*inputs)
TailwindMerge::Merger.new.merge(inputs.join(" "))
end
end
この関数を使うことで"w-4 w-full"
のようなバッティングするクラスがある場合に後の方(w-full
)を優先するということができるようになります。
buttonコンポーネントにclass_name
という引数を追加、viewファイルでclass_name
を優先してマージするように修正します。
# frozen_string_literal: true
class Button::Component < ApplicationViewComponent
option :variant, default: proc { :default }
option :size, default: proc { :default }
option :class_name, default: proc { "" } # 追加
# 省略
end
<button class="<%= tw_merge(style(variant:, size:), class_name) %>">
<%= content %>
</button>
クラスを追加してスタイルを上書きしてみます。
<div class="p-4 flex gap-4">
<%= component(:button) {"button"} %>
<%= component(:button, variant: :outline) {"outline"} %>
<%= component(:button, variant: :destructive) {"destructive"} %>
<%= component(:button, variant: :secondary) {"secondary"} %>
<%= component(:button, variant: :ghost) {"ghost"} %>
<%= component(:button, variant: :link) {"link"} %>
</div>
<div class="p-4 flex gap-4">
<%= component(:button) {"button"} %>
<%= component(:button, size: :lg) {"lg"} %>
<%= component(:button, size: :sm) {"sm"} %>
<%= component(:button, size: :icon, class_name: "w-full") {"icon"} %> <%# 変更 %>
</div>
ルートページにアクセスするとスタイルを上書きできていることが確認できます。

これでshadcn/ui
のボタンコンポーネントをRails
のViewComponent
を使って再現することができました。
参考
https://techracho.bpsinc.jp/hachi8833/2024_03_07/139774
https://github.com/palkan/view_component-contrib
https://github.com/shadcn-ui/ui