Documentation

Components

shadcn's component library, as source your app owns. whitehall add button copies Button.wh into src/lib/components/ui/, the way npx shadcn add button does.

whitehall add button
import $lib.components.ui.Button

<Button variant="outline" onClick={save}>Save</Button>

From then on the file is yours: edit its classes, add a variant, delete what you do not use. A second add refuses rather than overwrite it. Using <Button> without the import is a build error that names both steps.

Moving one at a time. The rows below marked source are whitehall add components today: aspect-ratio, badge, button, collapsible (with its trigger and content), kbd (with KbdGroup), label, marker, message-scroller, progress, separator, switch and toggle. whitehall add takes several names at once, and a name it does not know lists the ones it does. The rest of the library is still built in, generated from shadcn's own style-vega.css and switched by the preset: shadcn (the default) and none generate it, material3 swaps five tags for Material's own. See Theming.

The shape of a component

A component takes its variants as props. <Button> has shadcn's six variants and eight sizes:

<Button variant="destructive" size="sm" onClick={remove}>Delete</Button>

<Button variant="outline">
  <Icon name="lucide:plus" />
  Add item
</Button>
PropValues
variantdefault, secondary, destructive, outline, ghost, link
sizedefault, xs, sm, lg, and the four icon-only counterparts icon, icon-xs, icon-sm, icon-lg
default is the filled variant. shadcn has no variant called primary — primary is the token the default variant is painted with. Writing variant="primary" is an error that lists the six.

See it happen

Switch between these to see the props on the same tag, compiled live in your browser.

ActionButton.wh
import $lib.components.ui.Button

<Button onClick={save}>Save</Button>
Kotlin waiting
Scroll into view to compile.

The imported component, with an onClick and a text child.

Slots

A multi-part component is written the way shadcn writes it, as nested tags. Each part is a tag of its own:

<Card>
  <CardHeader>
    <CardTitle>Total revenue</CardTitle>
    <CardDescription>Last 30 days</CardDescription>
  </CardHeader>
  <CardContent>
    <Text class="text-3xl font-bold">$45,231.89</Text>
  </CardContent>
  <CardFooter>
    <Button variant="outline">View report</Button>
  </CardFooter>
</Card>

That is 217 tags across 55 roots. A slot tag only compiles inside its own parent — a <CardFooter> written anywhere else is an error rather than a call that does not exist.

Anchored components

Popovers, menus, tooltips and selects position themselves against whatever triggered them:

<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">Open</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>My account</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Profile</DropdownMenuItem>
    <DropdownMenuItem>Settings</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Writing your own

A component you write yourself is the same kind of file as Button.wh: it can take the caller's markup, and it can take the caller's classes.

Children

A component that wraps its caller's markup puts a <slot /> where that markup goes, the same as a layout does:

// src/components/Card.wh
<Box class="rounded-2xl border p-16">
  <slot />
</Box>
// anywhere
<Card>
  <Text>Hello</Text>
</Card>

The generated function takes Compose's trailing lambda, so the call site reads as a block rather than an argument — and the parameter is defaulted, so <Card /> with no children still compiles:

fun Card(modifier: Modifier = Modifier, content: @Composable () -> Unit = {})
Two refusals, both deliberate. Two <slot />s in one component would render the caller's markup twice, along with two copies of any state it holds — for a component with several holes, write several components, the way <CardHeader> and <CardContent> sit beside <Card> above. And <slot /> in a screen is an error: the router reaches a screen and passes it no markup. Only a layout takes the page below it.

class at the call site

A class on a component you wrote lands on that component's root element, the same as it would on a <Box>:

<UserCard class="mt-4 w-full" />
UserCard(modifier = Modifier.padding(top = 16.dp).fillMaxWidth())

The generated composable takes modifier: Modifier = Modifier — Compose's own convention — and applies it to its root before that root's own modifiers. A class from outside therefore layers underneath what the component draws, which is what makes <UserCard class="backdrop-blur-xl" /> frost the content behind the card rather than paint over it.

A component that declares @prop val class: String = "" takes the caller's classes itself instead, and puts them where it wants them — last in its own list, the way Button.wh does, so they win over its defaults as shadcn's cn() has it: <Row class={[base, variantClasses, class]}>.

A component with more than one root element takes one too: multiple roots are auto-wrapped in a Column, and the wrapper is what the class lands on. Screens and layouts do not take a class — nothing writes <HomeScreen class="…" />, the router reaches them.


The library

shadcn's own alphabetical order. Root tag is what you write; components with slots list the count of tags in the family.

Single tag

ComponentTag
Aspect Ratio<AspectRatio> — source: whitehall add aspect-ratio
Badge<Badge> — source: whitehall add badge
Button<Button> — source: whitehall add button
Calendar<Calendar>
Checkbox<Checkbox>
Input<TextField>
Label<Label> — source: whitehall add label
Progress<Progress> — source: whitehall add progress
Scroll Area<ScrollArea>
Separator<Separator> — source: whitehall add separator
Skeleton<Skeleton>
Slider<Slider>
Spinner<Spinner>
Switch<Switch> — source: whitehall add switch
Textarea<Textarea>
Toggle<Toggle> — source: whitehall add toggle
shadcn calls it Input. Whitehall's spelling stays <TextField>, because that is what every existing project already writes. It is the same component underneath.

With slots

ComponentRoot tagTags
Accordion<Accordion>4
Alert<Alert>4
Alert Dialog<AlertDialog>9
Attachment<Attachment>2
Avatar<Avatar>3
Breadcrumb<Breadcrumb>7
Bubble<Bubble>3
Button Group<ButtonGroup>3
Card<Card>6
Carousel<Carousel>5
Collapsible<Collapsible> — source: whitehall add collapsible3
Command<Command>8
Context Menu<ContextMenu>11
Dialog<Dialog>6
Drawer<Drawer>6
Dropdown Menu<DropdownMenu>11
Empty<Empty>6
Field<Field>10
Hover Card<HoverCard>3
Input Group<InputGroup>6
Input OTP<InputOTP>4
Item<Item>10
Kbd<Kbd> — source: whitehall add kbd2
Marker<Marker> — source: whitehall add marker1
Menubar<Menubar>12
Message<Message>2
Message Scroller<MessageScroller> — source: whitehall add message-scroller1
Native Select<NativeSelect>3
Navigation Menu<NavigationMenu>6
Pagination<Pagination>5
Popover<Popover>6
Radio Group<RadioGroup>2
Select<Select>7
Sheet<Sheet>6
Table<Table>8
Tabs<Tabs>4
Toast<Toast>1
Toggle Group<ToggleGroup>2
Tooltip<Tooltip>3

Compositions, not tags

Three of the 58 are shadcn recipes rather than components. They have no tag of their own:

ComponentWritten as
Combobox<Popover> + <Command>
Date Picker<Calendar> in a <Popover>
Typography<Text> plus the type scale classes

The five that are not here

Leaving one out is a decision, and the reasoning is written down rather than implied. Two are deferred; three do not apply.

ComponentStatusWhy
ChartDeferredNeeds a plotting layer under it. shadcn's is layerchart plus tokens; the port is the integration, and there is nothing to integrate yet.
Data TableDeferredNeeds <Table> plus sort/select state — a decision of its own, not paint.
SidebarNot portedA desktop shell. shadcn's own mobile fallback is literally a Sheet — the same primitive, imported unmodified — and <Sheet> is ported in full.
ResizableNot portedDesktop layout. There is no mouse and no window to split.
DirectionNot a componentRTL is a discipline, not a widget — it has no CSS of its own. Layout mirrors automatically, including drawn chevrons.

What does not survive the translation

The generated Kotlin is a transcription, not an interpretation. Where Compose cannot express what shadcn does, the gap is listed rather than approximated:

  • Focus rings. shadcn's focus-visible: ring is a keyboard affordance. A touch screen has no focus-visible state, so it is deliberately not ported.
  • Hover. hover: variants have no touch equivalent; the pressed state carries the feedback instead.
  • Text metrics. Android's text is a few pixels tighter than the browser's at the same size. This is a HarfBuzz-versus-browser difference, not a bug to be tuned out.

See Also