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" /> | Type | Effect |
|---|---|
password | Masked input (PasswordVisualTransformation) |
email | Email keyboard (@ and . keys) |
phone | Phone keyboard (numeric dial pad) |
number | Number keyboard |
url | URL 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>