brust 2.0.0-alpha.8
React & libraries / Set up shadcn/ui

Set up shadcn/ui

shadcn/ui copies its components into your project, so brust compiles them like your own code. This guide takes a new brust project to a page with a card, a button and a dialog, with the commands and files exactly as they were run.

1Create the project

You need Bun and a generated brust project. The blank starter already has Tailwind CSS v4 through brust’s own plugin, so there is no separate Tailwind build to configure.

~/
$ bun create @brust/app@latest my-app --yes
$ cd my-app && bun install

2Tell the CLI about it

The shadcn CLI’s init does not recognise brust as a framework, so it stops there. Skip it: add only needs two files.

~/my-app
$ bunx shadcn@latest init
We could not detect a supported framework at /home/you/my-app.
Visit https://ui.shadcn.com/docs/installation/manual to manually configure your project.

components.json at the project root says where things go: the style, the CSS file (app.css, the one brust’s Tailwind reads) and the aliases.

components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": { "config": "", "css": "app.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" },
  "iconLibrary": "lucide",
  "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }
}

The aliases start with @/, and the CLI resolves them through tsconfig.json. Add one line to its compilerOptions; no baseUrl is needed:

tsconfig.json (inside "compilerOptions")
"paths": { "@/*": ["./*"] },
Note

The imports the CLI writes into the components it adds (@/components/ui/button in the dialog) build as they are, with this one line.

3Add the theme and components

add index is the part of init you need: it adds the Tailwind imports, the light and dark colour variables and the @theme block to app.css, creates lib/utils.ts and installs shadcn and tw-animate-css. Then add the components. The CLI installs cn, the class-name helper the components import, as a package; there is no helper to write yourself.

~/my-app
$ bun add radix-ui lucide-react class-variance-authority tw-animate-css
$ bunx shadcn@latest add index -y
$ bunx shadcn@latest add button card dialog -y
app.css (what `add index` put at the top; the variables follow)
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";

@custom-variant dark (&:is(.dark *));

4Use them in a route

The components are files in components/ui/. Import them like any other component and add the page to routes.tsx.

pages/ShadcnPage.tsx
import { Button } from '../components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../components/ui/dialog'

export default function ShadcnPage() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>shadcn/ui in brust</CardTitle>
      </CardHeader>
      <CardContent>
        <Dialog>
          <DialogTrigger asChild>
            <Button>Open</Button>
          </DialogTrigger>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>Hello</DialogTitle>
              <DialogDescription>A Radix dialog, one React island.</DialogDescription>
            </DialogHeader>
          </DialogContent>
        </Dialog>
      </CardContent>
    </Card>
  )
}
routes.tsx
import ShadcnPage from './pages/ShadcnPage'
// in the children array, next to the home page:
{ path: '/ui', Component: ShadcnPage },

5Build and read the result

brust build names every Radix part that renders as a React island. These are warnings, not errors: the component works as it does in React. The card and the button are static markup: a page with only those loads no JavaScript of its own. The dialog is where React comes in, one island per Radix part, and in the browser the Open button shows the dialog.

~/my-app
$ bun run build
warning external-component components/ui/dialog.tsx:11:10 <DialogPrimitive.Root> comes from the package radix-ui (Dialog.Root); it renders as a React island
warning spread-props components/ui/dialog.tsx:11:10 props are spread into the React island <DialogPrimitive.Root>
…
[brust] built dist in 304ms

Run it with brust start, or work on it with dev mode. shadcn/ui and React libraries says which components compile and which stay islands, with the numbers from the build.

6Dark mode

add index writes the variables twice: the light values under :root and the dark values under .dark, and the dark: variant applies below an element with the dark class. Put that class on <html> to switch the whole page.