@prop
@prop declares a parameter on a component you write. A default makes it
optional.
Basic Usage
// UserCard.wh
@prop val name: String
@prop val email: String
<Card class="p-16">
<Text fontSize={20}>{name}</Text>
<Text color="#999">{email}</Text>
</Card> Use the component:
<UserCard name="Alice" email="alice@example.com" /> Default Values
@prop val name: String
@prop val age: Int = 18
@prop val verified: Boolean = false
<Text>{name} ({age}) {verified ? "✓" : ""}</Text> Optional Props
@prop val title: String
@prop val subtitle: String? = null
<Column>
<Text>{title}</Text>
@if (subtitle != null) {
<Text color="#999">{subtitle}</Text>
}
</Column> Literal-Union Props
@prop val size: "sm" | "default" | "lg" = "default" A type written as string literals is the set of values the prop takes. A caller writes one
of them, or an if whose branches are; anything else is a build error naming
the set. Inside the component the prop is a String.
See it happen
Required, defaulted, optional — same directive, three parameter shapes.
@prop val name: String
<Text>Hello, {name}</Text>@prop val name: String becomes a required parameter on the generated function — no default, so every caller must pass one.
Children
// Card.wh
@prop val title: String
<Box class="rounded-2xl border p-16">
<Text class="font-bold">{title}</Text>
<slot />
</Box> <slot /> is where the caller's children render. A component that wraps
its content this way, instead of taking a text prop, composes like any
built-in element:
<Card title="Hello">
<Text>Body</Text>
</Card> Two <slot />s in one component is a build error — it would render the
children twice; write two components instead. A screen has no children either, so <slot /> in a +screen.wh is also a build error.
A class passed to a component lands on its root element, before the root's
own classes. A class from the caller layers underneath whatever the component draws.
A component with several root elements is wrapped in a Column.
<Card title="Hello" class="mt-4">
<Text>Body</Text>
</Card> Naming a Component Like a Built-In
A component named like a built-in wins over it: import $lib.components.Alert beside <Alert> compiles yours.
Complex Types
data class User(val id: String, val name: String)
@prop val user: User
@prop val users: List<User>
@prop val onUserClick: (User) -> Unit
<LazyColumn>
@for (user in users) {
<Card onClick={() => onUserClick(user)}>
<Text>{user.name}</Text>
</Card>
}
</LazyColumn> Callback Props
@prop val onClick: () -> Unit
@prop val onSubmit: (String) -> Unit
var text = $state("")
<Column>
<TextField bind:value={text} />
<Button onClick={() => onSubmit(text)}>Submit</Button>
</Column> Usage:
<MyForm
onSubmit={value => println("Submitted: $value")}
/> Behind the Scenes
Whitehall transpiles @prop to Composable function parameters:
@prop val name: String
@prop val age: Int = 18
// Becomes:
@Composable
fun MyComponent(name: String, age: Int = 18) {
// ...
} Nested Components
// ProfileCard.wh
@prop val user: User
@prop val showActions: Boolean = true
<Card class="p-16">
<Row class="gap-12">
<Image src={user.avatar} w={48} h={48} />
<Column>
<Text fontWeight="bold">{user.name}</Text>
<Text color="#999">{user.email}</Text>
</Column>
</Row>
@if (showActions) {
<Button>View Profile</Button>
}
</Card>
// Usage
@for (user in users) {
<ProfileCard user={user} showActions={true} />
} Component Composition
// LabeledButton.wh
@prop val text: String
@prop val icon: String? = null
@prop val onClick: () -> Unit
<Button onClick={onClick}>
<Row class="gap-8">
@if (icon != null) {
<Icon name={icon} />
}
<Text>{text}</Text>
</Row>
</Button>
// IconButton.wh
@prop val icon: String
@prop val label: String
@prop val onClick: () -> Unit
<LabeledButton icon={icon} text={label} onClick={onClick} /> State in Components
Components can have internal state:
@prop val initialCount: Int = 0
@prop val onChange: (Int) -> Unit = {}
var count = $state(initialCount)
<Row class="gap-8">
<Button onClick={() => {
count--
onChange(count)
}}>-</Button>
<Text>{count}</Text>
<Button onClick={() => {
count++
onChange(count)
}}>+</Button>
</Row> Case Insensitive
All annotation styles work:
@prop val name: String
@Prop val name: String
@PROP val name: String A caller passing a value of the wrong type, or leaving out a required prop, is a build error naming the parameter — the same check Kotlin runs on any function call.
Complete Example
// ShowCard.wh
data class Show(val id: String, val name: String, val poster: String)
@prop val show: Show
@prop val onCardClick: (String) -> Unit
<Card
onClick={() => onCardClick(show.id)}
p={16}
class="rounded-lg"
>
<Image
src={show.poster}
w={120}
h={180}
fit="cover"
class="rounded"
/>
<Text
fontSize={16}
fontWeight="bold"
class="mt-2"
>
{show.name}
</Text>
</Card>
// Usage
val shows = [...]
<FlowRow gap={16}>
@for (show in shows) {
<ShowCard
show={show}
onCardClick={id => $navigate("/show/$id")}
/>
}
</FlowRow>