Documentation

Inputs

TextField, Checkbox and Switch collect input. A `var` made with `$state` and `bind:` is the whole binding — there is no separate form library.

TextField

<TextField label="Name" />

Two-way binding

Use bind:value to sync the field with state:

var email = $state("")

<TextField bind:value={email} label="Email" />
<Text>You typed: {email}</Text>

bind:value={email} is value = email, onValueChange = { email = it }. Typing writes back into email; anything else that reads email redraws.

See it happen

Switch between these to see binding and validation added one step at a time, compiled live.

EmailField.wh
<TextField label="Email" />
Kotlin waiting
Scroll into view to compile.

A TextField with no bind:value. It renders and accepts typing, but there is no var for it to write into — nothing else on the screen can react to what you type.

Input types

The type prop picks the keyboard and, for password, hides what is typed:

<TextField type="password" label="Password" />
<TextField type="email" label="Email" />
<TextField type="phone" label="Phone" />
<TextField type="number" label="Age" />
<TextField type="url" label="Website" />
TypeEffect
passwordHides what is typed
emailEmail keyboard
phonePhone keyboard
numberNumeric keyboard
urlURL keyboard

Placeholder

<TextField placeholder="Enter your name" />

Disabled

disabled is bare — no value means true. There is no enabled prop:

<TextField label="Username" disabled />
<TextField label="Username" disabled={isSaving} />

Autofocus

autofocus takes focus and raises the keyboard, both — a field that focuses without the keyboard looks ready and is not:

<TextField label="Search" autofocus />

Validation

There is no error prop. A validation message is an ordinary conditional element below the field, driven by a $derived value over the bound state:

var email = $state("")
val isValid = $derived(email.isEmpty() || email.contains("@"))

<Column class="gap-4">
  <TextField bind:value={email} label="Email" type="email" />
  @if (!isValid) {
    <Text class="text-destructive text-sm">Enter a valid email</Text>
  }
</Column>

Form validation

var email = $state("")
var password = $state("")

val emailValid = $derived(email.isEmpty() || email.contains("@"))
val passwordValid = $derived(password.isEmpty() || password.length >= 8)
val formValid = $derived(email.isNotEmpty() && password.isNotEmpty() && emailValid && passwordValid)

<Column class="gap-16">
  <Column class="gap-4">
    <TextField bind:value={email} label="Email" type="email" />
    @if (!emailValid) {
      <Text class="text-destructive text-sm">Enter a valid email</Text>
    }
  </Column>

  <Column class="gap-4">
    <TextField bind:value={password} label="Password" type="password" />
    @if (!passwordValid) {
      <Text class="text-destructive text-sm">At least 8 characters</Text>
    }
  </Column>

  <Button disabled={!formValid} onClick={submit}>Sign Up</Button>
</Column>

Checkbox and Switch

Use bind:checked for boolean inputs:

var agreedToTerms = $state(false)
var notificationsEnabled = $state(true)

<Row class="gap-8 items-center">
  <Checkbox bind:checked={agreedToTerms} />
  <Text>I agree to the terms</Text>
</Row>

<Row class="gap-8 items-center justify-between">
  <Text>Notifications</Text>
  <Switch bind:checked={notificationsEnabled} />
</Row>

Common patterns

Login form

var email = $state("")
var password = $state("")

suspend fun login() {
  $fetch.post(
    url = "https://api.example.com/login",
    body = { email: email, password: password }
  )
  $navigate("/home")
}

<Column class="gap-16 p-20">
  <Text class="text-2xl font-bold">Sign In</Text>

  <TextField bind:value={email} label="Email" type="email" />
  <TextField bind:value={password} label="Password" type="password" />

  <Button onClick={login} class="w-full">Sign In</Button>
</Column>

Search field

var query = $state("")

<TextField bind:value={query} placeholder="Search..." autofocus />

Settings form

var username = $state("alice")
var emailNotifications = $state(true)
var pushNotifications = $state(false)

<Column class="gap-20 p-16" scrollable>
  <Text class="text-xl font-bold">Profile Settings</Text>

  <TextField bind:value={username} label="Username" />

  <Text class="text-lg font-medium">Notifications</Text>

  <Row class="items-center justify-between">
    <Column class="flex-1 gap-4">
      <Text>Email notifications</Text>
      <Text class="text-sm text-muted-foreground">Receive updates by email</Text>
    </Column>
    <Switch bind:checked={emailNotifications} />
  </Row>

  <Row class="items-center justify-between">
    <Column class="flex-1 gap-4">
      <Text>Push notifications</Text>
      <Text class="text-sm text-muted-foreground">Receive push notifications</Text>
    </Column>
    <Switch bind:checked={pushNotifications} />
  </Row>

  <Button class="w-full">Save changes</Button>
</Column>

Prop reference

PropTypeDescription
labelStringField label
placeholderStringPlaceholder text
typeStringpassword · email · phone · number · url
disabledBooleanBare means true
autofocusBooleanTakes focus and raises the keyboard
bind:valueStringTwo-way binding to a $state var

See also

  • Button — submit forms and trigger actions
  • Text — labels and messages