Documentation

@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.

UserCard.wh
@prop val name: String

<Text>Hello, {name}</Text>
Kotlin waiting
Scroll into view to compile.

@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>

See Also