Overview
shadcn/ui v4 can target Base UI (@base-ui/react) or Radix. Composition APIs differ. Check the project's components.json / installed primitives before editing UI.
Neexo SaaS default: Base UI + render prop (not Radix asChild).
Choose the Primitive Stack
| Primitive | Composition | Typical signal |
|---|---|---|
| Base UI | render={...} |
@base-ui/react in dependencies |
| Radix | asChild |
@radix-ui/react-* in dependencies |
Do not mix composition styles across the same component set.
Base UI (Neexo SaaS default): render Prop
// Correct — Base UI / shadcn with Base primitives
<Button render={<Link href="/dashboard" />}>Go to Dashboard</Button>
// Wrong for Base UI — Radix-style asChild
<Button asChild><Link href="/dashboard">Go to Dashboard</Link></Button>
asChild does not exist on Base UI components.
Radix: asChild
When the project uses Radix primitives, keep Radix composition:
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
Component Installation
npx shadcn@latest add button dialog select
Components are installed to components/ui/. Do not barrel-export them — import directly:
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
Styling Conventions
- Use
cn()from@/lib/utilsfor conditional classes:import { cn } from "@/lib/utils"; <div className={cn("rounded-xl p-4", isActive && "ring-2 ring-primary")} /> - Use Tailwind utility classes — do not create custom CSS for component styling
- Respect existing design tokens in
globals.css - Generated
components/ui/files are exempt from file-size limits
Patterns (Base UI)
Dialog with Form
<Dialog>
<DialogTrigger render={<Button />}>Open</DialogTrigger>
<DialogContent>
<form action={submitAction}>
{/* form fields */}
</form>
</DialogContent>
</Dialog>
Select with Controlled Value
<Select value={selected} onValueChange={setSelected}>
<SelectTrigger>
<SelectValue placeholder="Choose..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="a">Option A</SelectItem>
<SelectItem value="b">Option B</SelectItem>
</SelectContent>
</Select>
Do Not
- Assume every shadcn v4 project is Base UI — verify primitives first
- Use
asChildon Base UI components - Create new button styles — use
variantprop on<Button> - Barrel-export from
components/ui/— import each component directly - Override shadcn component internals unless absolutely necessary