Skip to content

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.


  • Tailwind CSS configured in your Rails app
  • ruact installed and working (Getting Started)

Initialize shadcn/ui in your project:

Terminal window
npx shadcn@latest init

When 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.


Use the shadcn CLI to add individual components:

Terminal window
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add input

This copies the component source into app/javascript/components/ui/:

app/javascript/components/ui/
├── button.jsx
├── dialog.jsx
└── input.jsx

Step 3: Mark components as “use client”

Section titled “Step 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.


<!-- 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>

After adding new components, rebuild the manifest:

Terminal window
# Development — Vite dev server rebuilds automatically on file save
bin/dev
# Production
npm run build

Check that your components appear in the manifest:

Terminal window
cat public/react-client-manifest.json | grep -E "Button|Dialog"

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>
)
}

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”

Section titled “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/