Documentation

bind:

Two-way data binding for forms and inputs.

bind:value

Bind text input to a variable:

var email = ""

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

This is sugar for:

<TextField
  value={email}
  onValueChange={newValue => email = newValue}
  label="Email"
/>

bind:checked

Bind checkbox or switch state:

var enabled = false
var termsAccepted = false

<Switch bind:checked={enabled} label="Enable notifications" />
<Checkbox bind:checked={termsAccepted} label="I accept the terms" />

Form Example

var email = ""
var password = ""
var rememberMe = false

val isValid = email.isNotEmpty() && password.length >= 8

<Column class="gap-16 p-16">
  <TextField
    bind:value={email}
    label="Email"
    type="email"
  />

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

  <Checkbox
    bind:checked={rememberMe}
    label="Remember me"
  />

  <Button
    onClick={handleLogin}
    enabled={isValid}
    text="Login"
  />
</Column>

Text Field Types

Use type to control keyboard and input behavior:

var email = ""
var password = ""
var phone = ""
var age = ""
var website = ""

<TextField bind:value={email} type="email" label="Email" />
<TextField bind:value={password} type="password" label="Password" />
<TextField bind:value={phone} type="phone" label="Phone" />
<TextField bind:value={age} type="number" label="Age" />
<TextField bind:value={website} type="url" label="Website" />
TypeEffect
passwordMasked input (PasswordVisualTransformation)
emailEmail keyboard (@ and . keys)
phonePhone keyboard (numeric dial pad)
numberNumber keyboard
urlURL keyboard (/, . keys)

Validation

var email = ""
var error: String? = null

fun validateEmail() {
  error = if (email.contains("@")) null else "Invalid email"
}

<Column class="gap-8">
  <TextField
    bind:value={email}
    label="Email"
    onChange={validateEmail}
    isError={error != null}
  />

  @if (error != null) {
    <Text color="#ef4444" fontSize={12}>{error}</Text>
  }
</Column>

Live Search

var searchQuery = ""
var results: List<Show> = []

suspend fun search() {
  if (searchQuery.isEmpty()) {
    results = []
    return
  }
  results = $fetch("https://api.example.com/search?q=$searchQuery")
}

<Column class="gap-16">
  <TextField
    bind:value={searchQuery}
    label="Search"
    onChange={() => launch { search() }}
  />

  <LazyColumn>
    @for (result in results) {
      <ResultCard result={result} />
    }
  </LazyColumn>
</Column>

Multiple Switches

var notifications = true
var emailAlerts = false
var pushAlerts = true
var smsAlerts = false

<Column gap={12} p={16}>
  <Text fontSize={20}>Notification Settings</Text>

  <Switch
    bind:checked={notifications}
    label="Enable all notifications"
  />

  @if (notifications) {
    <Switch bind:checked={emailAlerts} label="Email alerts" />
    <Switch bind:checked={pushAlerts} label="Push alerts" />
    <Switch bind:checked={smsAlerts} label="SMS alerts" />
  }
</Column>

Number Inputs

var quantity = "1"

val quantityInt = quantity.toIntOrNull() ?: 0

<Column class="gap-8">
  <TextField
    bind:value={quantity}
    label="Quantity"
    type="number"
  />

  <Text>Total: ${price * quantityInt}</Text>

  <Button
    onClick={addToCart}
    enabled={quantityInt > 0}
    text="Add to Cart"
  />
</Column>

Password with Visibility Toggle

var password = ""
var showPassword = false

<TextField
  bind:value={password}
  type={showPassword ? "text" : "password"}
  label="Password"
  trailingIcon={
    <Icon
      name={showPassword ? "visibility-off" : "visibility"}
      onClick={() => showPassword = !showPassword}
    />
  }
/>

Behind the Scenes

Whitehall expands bind: to two-way binding:

bind:value={email}

// Becomes:
value = email
onValueChange = { email = it }

bind:checked={enabled}

// Becomes:
checked = enabled
onCheckedChange = { enabled = it }

Use bind: for forms and inputs to reduce boilerplate. It's more concise and readable than manual value/onChange pairs.

Complete Example

data class Profile(
  var name: String = "",
  var email: String = "",
  var bio: String = "",
  var notifications: Boolean = true
)

val profile = Profile()

var isSaving = false
var error: String? = null

suspend fun save() {
  val isValid = profile.name.isNotEmpty() && profile.email.contains("@")
  if (!isValid) {
    error = "Please fill all required fields"
    return
  }

  isSaving = true
  error = null
  try {
    $fetch.put(
      url = "https://api.example.com/profile",
      body = profile
    )
    $navigate("/profile")
  } catch (e: Exception) {
    error = e.message
  } finally {
    isSaving = false
  }
}

<Column class="gap-16 p-16">
  <Text fontSize={24}>Edit Profile</Text>

  <TextField
    bind:value={profile.name}
    label="Name"
    placeholder="Your name"
  />

  <TextField
    bind:value={profile.email}
    label="Email"
    type="email"
  />

  <TextField
    bind:value={profile.bio}
    label="Bio"
    minLines={3}
    maxLines={5}
  />

  <Switch
    bind:checked={profile.notifications}
    label="Enable notifications"
  />

  @if (error != null) {
    <Text color="#ef4444">{error}</Text>
  }

  <Button
    onClick={() => launch { save() }}
    enabled={!isSaving}
    text={isSaving ? "Saving..." : "Save"}
  />
</Column>

See Also

  • Inputs - TextField and input components
  • @prop - Component properties