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.


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:

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


Step 2: Add components

Use the shadcn CLI to add individual components:

bash
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"

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:

jsx
"use client"

export { Button } from "./ui/button"

Create app/javascript/components/Dialog.jsx:

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

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

bash
# Development — Vite dev server rebuilds automatically on file save
bin/dev

# Production
npm run build

Check that your components appear in the manifest:

bash
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:

erb
<%
  options = @categories.map { |c| { value: c.id.to_s, label: c.name } }
%>
<CategorySelect options={options} selectedValue={@post.category_id.to_s} />
jsx
// 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:

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/

Released under the MIT License.