shadcn/ui v4

Instructions for shadcn/ui v4 — choose Base UI or Radix primitives per project; Neexo SaaS default is Base UI with the render prop.

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

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/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 (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 asChild on Base UI components
  • 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 into your project — e.g. .instructions.md, .agent.md, or SKILL.md

## 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

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

```tsx
<Button asChild>
  <Link href="/dashboard">Go to Dashboard</Link>
</Button>
```

## 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 (Base UI)

### 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

- Assume every shadcn v4 project is Base UI — verify primitives first
- Use `asChild` on Base UI components
- 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

Next steps

Matched by shared tags and category. Browse all instructions