Documentation

Components

shadcn's component library, ported to Compose. 58 of shadcn's 63 components, written as <Tag>s in a .wh file and compiled to Kotlin.

These are generated from shadcn's own style-vega.css rather than wrapped around Material, so an app painted in shadcn's colors has shadcn's shapes too. You get them by default — <Button> is Whitehall's, not androidx.compose.material3.Button.

Which library you get is the preset's call. shadcn (the default) and none both generate this library — the difference is that none leaves it unpainted. material3 is the one that swaps it out for Material's own components, where only the six with a Material counterpart are available. See Theming.
A component you write wins over the built-in of the same name. import $app.components.Alert beside <Alert> and yours is the one that compiles — which is also how you fill a gap under material3.

The shape of a component

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

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

<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 primaryprimary is the token the default variant is painted with. Writing variant="primary" is an error that suggests default by name.

See it happen

This embed compiles a single file with no whitehall.toml, so it has no palette to read and variant is off the table — the props below are the ones that work with or without the preset. Switch between these to see props layer onto the same tag, compiled live in your browser.

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

A single-tag component with just onClick and text — the baseline the next two variants layer props onto.

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" text="View report" />
  </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" text="Open" />
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>My account</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Profile</DropdownMenuItem>
    <DropdownMenuItem>Settings</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Writing your own

Everything above ships with the preset. A component you write yourself gets the same two affordances — 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 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>
Badge<Badge>
Button<Button>
Calendar<Calendar>
Checkbox<Checkbox>
Input<TextField>
Label<Label>
Progress<Progress>
Scroll Area<ScrollArea>
Separator<Separator>
Skeleton<Skeleton>
Slider<Slider>
Spinner<Spinner>
Switch<Switch>
Textarea<Textarea>
Toggle<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>3
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>2
Marker<Marker>1
Menubar<Menubar>12
Message<Message>2
Message Scroller<MessageScroller>1
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