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.
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.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> | Prop | Values |
|---|---|
variant | default, secondary, destructive, outline, ghost, link |
size | default, 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 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.
<Button onClick={save} text="Save" />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 = {}) <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
| Component | Tag |
|---|---|
| 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> |
<TextField>,
because that is what every existing project already writes. It is the same component
underneath.With slots
| Component | Root tag | Tags |
|---|---|---|
| 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:
| Component | Written 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.
| Component | Status | Why |
|---|---|---|
| Chart | Deferred | Needs a plotting layer under it. shadcn's is layerchart plus tokens; the port is the integration, and there is nothing to integrate yet. |
| Data Table | Deferred | Needs <Table> plus sort/select state — a decision of its own, not paint. |
| Sidebar | Not ported | A desktop shell. shadcn's own mobile fallback is literally a Sheet — the same primitive, imported unmodified — and <Sheet> is ported in full. |
| Resizable | Not ported | Desktop layout. There is no mouse and no window to split. |
| Direction | Not a component | RTL 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
- Theming — the preset that turns these on
- Tailwind Classes — the utility classes components are styled with
- Layout —
<Column>,<Row>,<Box>