Using shadcn/ui Components
shadcn/ui provides beautifully designed React components that you own and copy into your project. Since shadcn/ui components use hooks and browser APIs, they must be "use client" components in ruact.
Prerequisites
- Tailwind CSS configured in your Rails app
- ruact installed and working (Getting Started)
Step 1: Set up shadcn/ui
Initialize shadcn/ui in your project:
npx shadcn@latest initWhen prompted, choose the style, base color, and CSS variable settings. shadcn/ui will update your tailwind.config.js and create app/javascript/lib/utils.js.
Step 2: Add components
Use the shadcn CLI to add individual components:
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add inputThis copies the component source into app/javascript/components/ui/:
app/javascript/components/ui/
├── button.jsx
├── dialog.jsx
└── input.jsxStep 3: Mark components as "use client"
shadcn/ui components use React hooks internally. You need to re-export them as "use client" wrappers so ruact can register them in the manifest.
Create app/javascript/components/Button.jsx:
"use client"
export { Button } from "./ui/button"Create app/javascript/components/Dialog.jsx:
"use client"
export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"Why a wrapper file? shadcn/ui components do not include
"use client"directives by default. The wrapper file is where you declare the boundary. The"use client"must be in the file that ruact scans — the one at the top of the import chain.
Step 4: Use components in ERB templates
<!-- Passes props from Ruby to the Button client component -->
<Button variant="outline" size="sm">
View Details
</Button>
<!-- Open a dialog from server data -->
<Dialog>
<DialogTrigger asChild>
<Button>Open Post</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle><%= @post.title %></DialogTitle>
</DialogHeader>
<p><%= @post.body %></p>
</DialogContent>
</Dialog>Step 5: Verify the manifest
After adding new components, rebuild the manifest:
# Development — Vite dev server rebuilds automatically on file save
bin/dev
# Production
npm run buildCheck that your components appear in the manifest:
cat public/react-client-manifest.json | grep -E "Button|Dialog"Passing complex props
shadcn/ui's Select component requires structured option data. Pass it from Ruby:
<%
options = @categories.map { |c| { value: c.id.to_s, label: c.name } }
%>
<CategorySelect options={options} selectedValue={@post.category_id.to_s} />// app/javascript/components/CategorySelect.jsx
"use client"
import { useState } from "react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
export function CategorySelect({ options, selectedValue }) {
const [value, setValue] = useState(selectedValue)
return (
<Select value={value} onValueChange={setValue}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{options.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
)
}Theming
shadcn/ui uses CSS variables for theming. Configure them in your app/assets/stylesheets/application.css or app/javascript/application.css:
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
/* ... other variables from shadcn init output */
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
/* ... */
}Tip: Keep ui/ components un-"use client"
The files in ui/ (generated by shadcn) should remain as-is — do not add "use client" to them directly. This gives you the flexibility to:
- Import them into other client components without triggering extra manifest entries
- Keep generated shadcn files easy to update with
npx shadcn add - Control the public component API via your wrapper files in
components/