shadcn/ui v4 (Base UI)

Instructions for shadcn/ui v4 which uses Base UI primitives — use the render prop for composition, not Radix asChild.

AuthorNeexoCore
Apply to**/*.{ts,tsx}, **/components/**
Updated
shadcnuibase-uireact

Overview

shadcn/ui v4 switched from Radix UI to Base UI (@base-ui/react) under the hood. The most important change is how composition works.

Critical: render Prop, Not asChild

// ✅ Correct — shadcn/ui v4 (Base UI)
<Button render={<Link href="/dashboard" />}>Go to Dashboard</Button>

// ❌ Wrong — this is Radix UI / shadcn v3 syntax
<Button asChild><Link href="/dashboard">Go to Dashboard</Link></Button>

This is the most common mistake when moving to shadcn/ui v4. asChild does not exist in Base UI components.

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/utils for 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

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

  • Use asChild — it does not exist in Base UI
  • Create new button styles — use variant prop on <Button>
  • Barrel-export from components/ui/ — import each component directly
  • Override shadcn component internals unless absolutely necessary

Raw content

Copy this into your project — e.g. .instructions.md, .agent.md, or SKILL.md

## Overview

shadcn/ui v4 switched from Radix UI to Base UI (`@base-ui/react`) under the hood. The most important change is how composition works.

## Critical: render Prop, Not asChild

```tsx
// ✅ Correct — shadcn/ui v4 (Base UI)
<Button render={<Link href="/dashboard" />}>Go to Dashboard</Button>

// ❌ Wrong — this is Radix UI / shadcn v3 syntax
<Button asChild><Link href="/dashboard">Go to Dashboard</Link></Button>
```

This is the most common mistake when moving to shadcn/ui v4. `asChild` does not exist in Base UI components.

## Component Installation

```bash
npx shadcn@latest add button dialog select
```

Components are installed to `components/ui/`. Do not barrel-export them — import directly:

```tsx
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
```

## Styling Conventions

- Use `cn()` from `@/lib/utils` for conditional classes:
  ```tsx
  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

### Dialog with Form

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>Open</DialogTrigger>
  <DialogContent>
    <form action={submitAction}>
      {/* form fields */}
    </form>
  </DialogContent>
</Dialog>
```

### Select with Controlled Value

```tsx
<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

- Use `asChild` — it does not exist in Base UI
- Create new button styles — use `variant` prop on `<Button>`
- Barrel-export from `components/ui/` — import each component directly
- Override shadcn component internals unless absolutely necessary