Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Add, manage, and compose shadcn/ui components with correct patterns, styling, and CLI workflows.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/base-vs-radix.md
1# Base vs Radix23API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.45## Contents67- Composition: asChild vs render8- Button / trigger as non-button element9- Select (items prop, placeholder, positioning, multiple, object values)10- ToggleGroup (type vs multiple)11- Slider (scalar vs array)12- Accordion (type and defaultValue)1314---1516## Composition: asChild (radix) vs render (base)1718Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.1920**Incorrect:**2122```tsx23<DialogTrigger>24<div>25<Button>Open</Button>26</div>27</DialogTrigger>28```2930**Correct (radix):**3132```tsx33<DialogTrigger asChild>34<Button>Open</Button>35</DialogTrigger>36```3738**Correct (base):**3940```tsx41<DialogTrigger render={<Button />}>Open</DialogTrigger>42```4344This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.4546---4748## Button / trigger as non-button element (base only)4950When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.5152**Incorrect (base):** missing `nativeButton={false}`.5354```tsx55<Button render={<a href="/docs" />}>Read the docs</Button>56```5758**Correct (base):**5960```tsx61<Button render={<a href="/docs" />} nativeButton={false}>62Read the docs63</Button>64```6566**Correct (radix):**6768```tsx69<Button asChild>70<a href="/docs">Read the docs</a>71</Button>72```7374Same for triggers whose `render` is not a `Button`:7576```tsx77// base.78<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>79Pick date80</PopoverTrigger>81```8283---8485## Select8687**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.8889**Incorrect (base):**9091```tsx92<Select>93<SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger>94</Select>95```9697**Correct (base):**9899```tsx100const items = [101{ label: "Select a fruit", value: null },102{ label: "Apple", value: "apple" },103{ label: "Banana", value: "banana" },104]105106<Select items={items}>107<SelectTrigger>108<SelectValue />109</SelectTrigger>110<SelectContent>111<SelectGroup>112{items.map((item) => (113<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>114))}115</SelectGroup>116</SelectContent>117</Select>118```119120**Correct (radix):**121122```tsx123<Select>124<SelectTrigger>125<SelectValue placeholder="Select a fruit" />126</SelectTrigger>127<SelectContent>128<SelectGroup>129<SelectItem value="apple">Apple</SelectItem>130<SelectItem value="banana">Banana</SelectItem>131</SelectGroup>132</SelectContent>133</Select>134```135136**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.137138**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.139140```tsx141// base.142<SelectContent alignItemWithTrigger={false} side="bottom">143144// radix.145<SelectContent position="popper">146```147148---149150## Select — multiple selection and object values (base only)151152Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.153154**Correct (base — multiple selection):**155156```tsx157<Select items={items} multiple defaultValue={[]}>158<SelectTrigger>159<SelectValue>160{(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}161</SelectValue>162</SelectTrigger>163...164</Select>165```166167**Correct (base — object values):**168169```tsx170<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>171<SelectTrigger>172<SelectValue>{(value) => value.name}</SelectValue>173</SelectTrigger>174...175</Select>176```177178---179180## ToggleGroup181182Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.183184**Incorrect (base):**185186```tsx187<ToggleGroup type="single" defaultValue="daily">188<ToggleGroupItem value="daily">Daily</ToggleGroupItem>189</ToggleGroup>190```191192**Correct (base):**193194```tsx195// Single (no prop needed), defaultValue is always an array.196<ToggleGroup defaultValue={["daily"]} spacing={2}>197<ToggleGroupItem value="daily">Daily</ToggleGroupItem>198<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>199</ToggleGroup>200201// Multi-selection.202<ToggleGroup multiple>203<ToggleGroupItem value="bold">Bold</ToggleGroupItem>204<ToggleGroupItem value="italic">Italic</ToggleGroupItem>205</ToggleGroup>206```207208**Correct (radix):**209210```tsx211// Single, defaultValue is a string.212<ToggleGroup type="single" defaultValue="daily" spacing={2}>213<ToggleGroupItem value="daily">Daily</ToggleGroupItem>214<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>215</ToggleGroup>216217// Multi-selection.218<ToggleGroup type="multiple">219<ToggleGroupItem value="bold">Bold</ToggleGroupItem>220<ToggleGroupItem value="italic">Italic</ToggleGroupItem>221</ToggleGroup>222```223224**Controlled single value:**225226```tsx227// base — wrap/unwrap arrays.228const [value, setValue] = React.useState("normal")229<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>230231// radix — plain string.232const [value, setValue] = React.useState("normal")233<ToggleGroup type="single" value={value} onValueChange={setValue}>234```235236---237238## Slider239240Base accepts a plain number for a single thumb. Radix always requires an array.241242**Incorrect (base):**243244```tsx245<Slider defaultValue={[50]} max={100} step={1} />246```247248**Correct (base):**249250```tsx251<Slider defaultValue={50} max={100} step={1} />252```253254**Correct (radix):**255256```tsx257<Slider defaultValue={[50]} max={100} step={1} />258```259260Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast:261262```tsx263// base.264const [value, setValue] = React.useState([0.3, 0.7])265<Slider value={value} onValueChange={(v) => setValue(v as number[])} />266267// radix.268const [value, setValue] = React.useState([0.3, 0.7])269<Slider value={value} onValueChange={setValue} />270```271272---273274## Accordion275276Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.277278**Incorrect (base):**279280```tsx281<Accordion type="single" collapsible defaultValue="item-1">282<AccordionItem value="item-1">...</AccordionItem>283</Accordion>284```285286**Correct (base):**287288```tsx289<Accordion defaultValue={["item-1"]}>290<AccordionItem value="item-1">...</AccordionItem>291</Accordion>292293// Multi-select.294<Accordion multiple defaultValue={["item-1", "item-2"]}>295<AccordionItem value="item-1">...</AccordionItem>296<AccordionItem value="item-2">...</AccordionItem>297</Accordion>298```299300**Correct (radix):**301302```tsx303<Accordion type="single" collapsible defaultValue="item-1">304<AccordionItem value="item-1">...</AccordionItem>305</Accordion>306```307