brust 2.0.0-alpha.8
Guide / shadcn/ui and libraries

shadcn/ui and React libraries

shadcn/ui copies its components into your project, so brust compiles them like your own code. Most of the markup becomes static or native; what needs Radix’s behaviour stays a React island, and the build says which.

What compiles

  • Rest props and spread. { className, ...props } in the signature and {...props} on an element, with handlers that come through the spread.
  • asChild and Slot. Decided where the component is used, as below.
  • Constant children. Markup between the tags of a React island, when it is constant, travels into the island as data, so the page around it stays static. That includes a constant list ([1, 2, 3].map(…), Array.from({ length: 4 })), a constant condition and an element passed as a prop.
  • forwardRef. A component wrapped in forwardRef compiles; a forwarded ref on an HTML element is dropped, and any other use of it keeps the component on React.
  • Member tags. <DialogPrimitive.Root> resolves to the package export.
  • Components exported by name. A file with many components (Card, CardHeader, …) is fine; any of them may be an island.
// components/ui/button.tsx, as shadcn writes it
function Button({ className, variant, size, asChild = false, ...props }) {
  const Comp = asChild ? Slot.Root : 'button'
  return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
}

// pages/Home.tsx
<Button>Save</Button>                              // a <button>, static markup
<Button asChild><a href="/login">Login</a></Button>  // the <a>, with the button's props
import { Dialog as DialogPrimitive } from 'radix-ui'

function Dialog({ ...props }) {
  return <DialogPrimitive.Root data-slot="dialog" {...props} />   // the package export, one island
}

What stays an island

The Radix primitives themselves (dialogs, menus, tabs, selects) use React context, refs, portals and focus management, which brust does not lower yet, so each one is a React island of the package’s own export. Components that wrap a React library (a chart, a calendar, a carousel, a command palette) are islands too. A function or a value that changes with state passed to an island makes the component that passes it render with React, and the build reports it as island-prop.

Note

An island is not an error. The component works as it does in React; the page keeps its tier for everything around it.

The numbers

The fixture app builds each of 64 pages alone, one per component of shadcn/ui ([email protected], registry new-york-v4, 61 components, radix-ui 1.8.1, React 19.3.0). 64 build: 61 are static, 1 native and 2 React. Every page builds. Together they hold 53 React islands; 18 pages load no JavaScript at all. The JavaScript a page loads, gzipped, is 0 KB at the least, 87 KB in the middle and 240 KB at the most.

Across the libraries behind them (76 packages, 561 components) the compiler reads 1 as static or native today; the rest are the work still ahead.

What blocks most pages

CodePagesMeaning
external-component43The component comes from a package. Radix primitives are compiled by React, so they render as an island of the package export.
spread-props41Props are spread onto something that is not a plain element: a React island, or a value whose shape is not known when you build.
island-children25An island receives children that are not constant (they read state, hold a handler, or call a function).
hook-unsupported6A React hook brust does not lower yet, such as `useContext` or `useReducer`.
member-tag4A member tag of a local object, such as a context provider, or deeper than one level.
control-flow3Control flow in the component body that is not a single return.
jsx-expression3A JSX expression the compiler cannot lower, such as a render function.
slot-dynamic3

Page by page

PageTierIslandsJavaScript (gzip)
accordionstatic194 KB
alertstatic00
alert-dialogstatic1100 KB
aspect-ratiostatic174 KB
attachmentstatic00
avatarstatic287 KB
badgestatic00
badge-aslinkstatic00
breadcrumbstatic285 KB
bubblestatic00
buttonstatic00
button-aliasstatic185 KB
button-aschildstatic00
button-groupstatic186 KB
calendarstatic1107 KB
cardstatic00
carouselstatic196 KB
chartstatic1240 KB
checkboxstatic292 KB
collapsiblestatic189 KB
comboboxreact1158 KB
commandstatic1104 KB
context-menustatic1116 KB
dialogstatic1102 KB
directionstatic170 KB
drawerstatic1108 KB
dropdown-menustatic1117 KB
emptystatic00
fieldstatic186 KB
formreact1101 KB
hover-cardstatic1103 KB
inputstatic00
input-groupstatic187 KB
input-otpstatic187 KB
itemstatic00
kbdstatic00
labelstatic185 KB
markerstatic00
menubarstatic1116 KB
messagestatic00
message-scrollerstatic194 KB
native-selectstatic00
navigation-menustatic199 KB
paginationstatic287 KB
popoverstatic1109 KB
progressstatic185 KB
radio-groupstatic196 KB
resizablestatic1103 KB
scroll-areastatic192 KB
selectstatic1118 KB
separatorstatic284 KB
sheetstatic1101 KB
sidebarstatic1116 KB
skeletonstatic00
sliderstatic193 KB
sonnernative187 KB
spinnerstatic182 KB
switchstatic289 KB
tablestatic00
tabsstatic194 KB
textareastatic00
togglestatic287 KB
toggle-groupstatic193 KB
tooltipstatic1105 KB

Read it yourself

The same data, with the full list of blockers and the library analysis, is in docs/shadcn-coverage.md. bun run shadcn in the brust repository regenerates both files; a pull request with a stale copy fails CI. Sizes are gzipped and rounded to 1 KB; a page that loads no JavaScript shows 0. To see where your own app stands, run brust build: it prints a rule and a line for every component that falls back, and dist/manifest.json has each component’s tier.