docs / guides
Using shadcn/ui Components
Set up shadcn/ui in a ruact app, scaffold resources with it, and use its components in your own client components.
shadcn/ui gives you React components as source files you own, styled with Tailwind. In a ruact app they live in app/javascript/components/ui/ and you use them from your own client components. This page covers the setup, the --shadcn scaffold, and using the components in code you write.
ruact itself needs none of this: the default scaffold is plain HTML. shadcn is opt-in.
Step 1: Prepare the app
Section titled “1. Prepare the app”bin/rails generate ruact:install --shadcnThis is the same install generator. Run it on a new app, or on one where ruact is already installed. With --shadcn it also writes what shadcn’s CLI checks for and refuses to start without:
app/javascript/styles/globals.css— the Tailwind entry. shadcn adds its design tokens to this file.tsconfig.json— the@/*→app/javascript/*import alias.- Tailwind in
package.json—tailwindcss,@tailwindcss/cliandtw-animate-css, plus abuild:cssscript. - A
cssprocess inProcfile.dev—bin/devrebuildsapp/assets/builds/tailwind.cssas you edit.
On an app that already has these files, it adds what is missing and keeps what you have. When it cannot finish the setup, it says so and names the fix instead of reporting the prerequisites as in place. That happens with a Tailwind older than 4, or with a Procfile.dev that already runs a different CSS watcher, such as tailwindcss-rails’ bin/rails tailwindcss:watch, which compiles its own entry rather than the one shadcn writes into. Then it installs the npm packages and prints the two commands it does not run for you.
Step 2: Initialize shadcn
Section titled “2. Initialize shadcn”npx shadcn@latest init --base radixnpx shadcn@latest add button input textarea switch select label badge table alert-dialog dropdown-menuThey are interactive and go to the network, which is why the generator prints them instead of running them. init asks for a style preset; any of them works.
--base radix is load-bearing. shadcn now defaults to Base UI, while the components ruact:scaffold --shadcn generates import Radix primitives. Accept the default and those imports do not resolve.
The add list is what the scaffold uses. Add any other component the same way: npx shadcn@latest add dialog.
Step 3: Scaffold with shadcn
Section titled “3. Scaffold with shadcn”bin/rails generate ruact:scaffold Post title:string body:text published:boolean --shadcnbin/rails db:migrateThis is the same scaffold with the same controller, views and query, but the components are built from shadcn:
PostList— the list is aTable, with aDropdownMenufor the row actions on narrow screens.PostForm— the form usesInput,Textarea,SwitchandSelect.PostDeleteDialog— the confirmation is anAlertDialog.
Before writing anything, the scaffold checks that shadcn is set up and that the components it imports are installed. If they are not, it prints the commands that fix it and writes no file. The scaffold reference has every option.
Run bin/dev and open http://localhost:3000/posts.
Step 4: Use shadcn components in your own code
Section titled “4. Use shadcn components in your own code”A shadcn component is a building block for your client component. Import it from @/components/ui/… in a file that starts with "use client":
"use client"
import { useState } from "react"import { Button } from "@/components/ui/button"
export function LikeButton({ likes }: { likes: number }) { const [liked, setLiked] = useState(false)
return ( <Button variant={liked ? "default" : "outline"} onClick={() => setLiked(!liked)}> {liked ? `❤️ ${likes + 1}` : `🤍 ${likes}`} </Button> )}Then render your component from ERB, passing data as props:
<LikeButton likes={12} />Compose the parts in ERB. shadcn ships a component as several parts that share state, like Dialog, DialogTrigger and DialogContent. Write them as tags in the template, the way you would in JSX. The content between the tags is ERB, so it can hold your Ruby data, Rails helpers and other components:
<Dialog> <DialogTrigger>Preview</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle><%= @post.title %></DialogTitle> </DialogHeader> <p><%= @post.body %></p> <LikeButton likes={12} /> </DialogContent></Dialog>This needs ruact 0.0.19 or later, and every part’s file has to start with "use client": that line is what makes its exports tags. shadcn removes it from the files it writes unless components.json says "rsc": true, and shadcn init sets "rsc": false in any app that is not Next.js.
There are two ways to get the line:
- Add it by hand to the files you compose in ERB. For the dialog above, put
"use client"at the top ofcomponents/ui/dialog.tsx. - Set
"rsc": trueincomponents.json. Every component you add from then on keeps its"use client", and each of those files becomes a component ruact registers and bundles, whether a template uses it or not. Its exports also share one namespace with your own components: a shadcnSelectreplaces your ownSelectin templates.
Either way, npx shadcn@latest add <name> --overwrite rewrites the component’s files and the shadcn files it depends on. Re-adding dialog also rewrites button.tsx, which discards your edits and, with "rsc": false, the "use client" you added.
Without the line, <Dialog> in a template raises Ruact::UnknownComponentError. Some files, such as button and card, never have it, because shadcn does not mark them as client components. Add it yourself to use them as tags, for example <DialogTrigger asChild><Button variant="outline">Preview</Button></DialogTrigger>.
When the composition has its own logic, such as state, effects or event handlers, write it in a client component and pass it the data from ERB:
"use client"
import { useState } from "react"import { Button } from "@/components/ui/button"import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog"
export function PostPreview({ title, body }: { title: string; body: string }) { const [open, setOpen] = useState(false) return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="outline">Preview</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>{title}</DialogTitle> </DialogHeader> <p>{body}</p> <Button onClick={() => setOpen(false)}>Done</Button> </DialogContent> </Dialog> )}<PostPreview title={@post.title} body={@post.body} />(dialog is not in the scaffold’s list. Add it first with npx shadcn@latest add dialog.)
Styling and theming
Section titled “Styling and theming”shadcn init writes its theme — the CSS variables every component reads — into app/javascript/styles/globals.css, below the Tailwind import. Change colours and radii there. The css process rebuilds app/assets/builds/tailwind.css, and ruact’s layout links it through config.layout_stylesheets in config/initializers/ruact.rb. If your setup needs an extra line for that, the install generator prints it.
If the components render unstyled, check that bin/dev is running all three processes: web, vite and css. Without css, app/assets/builds/tailwind.css is never built.
shadcn versions
Section titled “shadcn versions”ruact:scaffold --shadcn reads the shadcn version from your package.json. If the major version is not one ruact has been run against, the scaffold prints a warning and generates the components anyway. The tested majors are 1, 2 and 4; 4 is what npx shadcn@latest installs today. If you have checked a newer major yourself and the generated components work, add it to the list:
Ruact.configure do |config| config.shadcn_compatible_versions = [1, 2, 4, 5]end