Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt
Use this file to discover all available pages before exploring further.
Kael’s form controls follow a single, consistent pattern: every control is controlled, meaning you supply the current value and an on_change callback that receives the next value. There is no internal state you cannot observe — the framework stores enough per-element history to support undo and redo automatically, but your entity owns the source of truth. This makes it straightforward to validate, transform, or persist values as users interact with your UI.
Text input: text_input()
text_input() is a fully-featured editable text field with Unicode grapheme navigation, clipboard support, undo/redo history, optional password masking, and multiline editing.
pub fn text_input(id: impl Into<ElementId>, text: impl Into<SharedString>) -> TextInput
impl TextInput {
pub fn placeholder(self, placeholder: impl Into<SharedString>) -> Self
pub fn multi_line(self) -> Self
pub fn max_lines(self, max_lines: usize) -> Self
pub fn password(self) -> Self
pub fn mask(self, mask: impl InputMask) -> Self
pub fn on_change(self, listener: impl Fn(SharedString, &mut Window, &mut App) + 'static) -> Self
pub fn on_submit(self, listener: impl Fn(SharedString, &mut Window, &mut App) + 'static) -> Self
}
Single-line field
use kael::prelude::*;
// In your Render impl:
let value = self.username.clone(); // SharedString stored on your entity
text_input("username", value)
.placeholder("Enter username…")
.on_change(cx.listener(|this, text: SharedString, _window, _cx| {
this.username = text;
}))
.on_submit(cx.listener(|this, _text, _window, cx| {
this.submit(cx);
}))
Password field
text_input("password", self.password.clone())
.placeholder("Password")
.password()
.on_change(cx.listener(|this, text: SharedString, _, _| {
this.password = text;
}))
Multiline field
text_input("notes", self.notes.clone())
.multi_line()
.max_lines(8)
.placeholder("Add a note…")
.on_change(cx.listener(|this, text: SharedString, _, _| {
this.notes = text;
}))
on_change fires after every keystroke with the full field value, not just the delta. Store the SharedString directly on your entity — do not try to diff it against the previous value.
Checkbox: checkbox()
checkbox() is a controlled boolean toggle with optional indeterminate state, a visible label, and full keyboard and accessibility support.
pub fn checkbox(id: impl Into<ElementId>, checked: bool) -> Checkbox
impl Checkbox {
pub fn label(self, label: impl Into<SharedString>) -> Self
pub fn indeterminate(self, indeterminate: bool) -> Self
pub fn disabled(self) -> Self
pub fn on_change(self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self
}
checkbox("agree-to-terms", self.agreed)
.label("I agree to the terms of service")
.on_change(cx.listener(|this, checked: &bool, _, _| {
this.agreed = *checked;
}))
Indeterminate state
Use .indeterminate(true) when a parent checkbox represents a mixed selection. Clicking an indeterminate checkbox always moves to the checked state.
checkbox("select-all", self.all_selected)
.indeterminate(self.some_selected && !self.all_selected)
.label("Select all")
.on_change(cx.listener(|this, checked: &bool, _, _| {
this.set_all_selected(*checked);
}))
Toggle: toggle()
toggle() is a controlled on/off switch — visually distinct from a checkbox but with an identical behavioral contract.
pub fn toggle(id: impl Into<ElementId>, on: bool) -> Toggle
impl Toggle {
pub fn label(self, label: impl Into<SharedString>) -> Self
pub fn disabled(self) -> Self
pub fn on_change(self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self
}
toggle("dark-mode", self.dark_mode_on)
.label("Dark mode")
.on_change(cx.listener(|this, on: &bool, _, cx| {
this.dark_mode_on = *on;
cx.update_global::<Theme, _>(|theme, _| {
*theme = if *on { Theme::dark() } else { Theme::light() };
});
}))
Both checkbox() and toggle() use on_change: impl Fn(&bool, …) — the new state is passed by reference. Dereference it with *on or *checked when storing.
Slider: slider()
slider() is a controlled range input that maps a f64 value onto a visual track. It supports horizontal and vertical orientations, optional discrete stepping, and drag-based interaction.
pub fn slider(id: impl Into<ElementId>, value: f64) -> Slider
impl Slider {
pub fn min(self, min: f64) -> Self
pub fn max(self, max: f64) -> Self
pub fn step(self, step: f64) -> Self
pub fn discrete(self) -> Self
pub fn vertical(self) -> Self
pub fn disabled(self) -> Self
pub fn on_change(self, listener: impl Fn(&f64, &mut Window, &mut App) + 'static) -> Self
}
Continuous slider
The slider defaults to min: 0.0, max: 100.0, and step: 1.0.
slider("volume", self.volume)
.min(0.0)
.max(1.0)
.step(0.01)
.on_change(cx.listener(|this, value: &f64, _, _| {
this.volume = *value;
}))
Discrete (snapping) slider
slider("rating", self.rating as f64)
.min(1.0)
.max(5.0)
.step(1.0)
.discrete()
.on_change(cx.listener(|this, value: &f64, _, _| {
this.rating = *value as u32;
}))
Vertical slider
slider("brightness", self.brightness)
.min(0.0)
.max(100.0)
.vertical()
.on_change(cx.listener(|this, value: &f64, _, _| {
this.brightness = *value;
}))
Undo and redo (Cmd+Z / Cmd+Shift+Z) are supported automatically. The history entry is committed when the user releases the drag, not on every intermediate value.
Select: select()
select() is a controlled combo box backed by a popup option list. Options can be any Clone + PartialEq type — typically an enum or a string. You can pass options as (value, label) tuples, which convert automatically via From<(T, L)> for SelectOption<T>.
pub fn select<T, I, O>(id: impl Into<ElementId>, value: T, options: I) -> Select<T>
where
T: Clone + PartialEq + 'static,
I: IntoIterator<Item = O>,
O: Into<SelectOption<T>>
impl Select<T> {
pub fn placeholder(self, placeholder: impl Into<SharedString>) -> Self
pub fn searchable(self) -> Self
pub fn on_change(self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self
}
Enum-backed select
#[derive(Clone, PartialEq)]
enum Country { Us, Uk, De, Fr }
select(
"country",
self.country.clone(),
[
(Country::Us, "United States"),
(Country::Uk, "United Kingdom"),
(Country::De, "Germany"),
(Country::Fr, "France"),
],
)
.placeholder("Choose a country…")
.on_change(cx.listener(|this, value: &Country, _, _| {
this.country = value.clone();
}))
Searchable select
Add .searchable() to show an in-popup search field that filters options by label.
select("font", self.font.clone(), available_fonts)
.searchable()
.placeholder("Select font…")
.on_change(cx.listener(|this, value: &SharedString, _, _| {
this.font = value.clone();
}))
Date picker: date_picker()
date_picker() is a controlled calendar picker backed by the time::Date type. It opens a month-grid popup and supports optional minimum and maximum date bounds.
pub fn date_picker(id: impl Into<ElementId>, date: Date) -> DatePicker
impl DatePicker {
pub fn min(self, min: Date) -> Self
pub fn max(self, max: Date) -> Self
pub fn on_change(self, listener: impl Fn(&Date, &mut Window, &mut App) + 'static) -> Self
}
use time::{Date, Month};
let today = Date::from_calendar_date(2026, Month::May, 16).unwrap();
date_picker("due-date", self.due_date)
.min(today)
.on_change(cx.listener(|this, date: &Date, _, _| {
this.due_date = *date;
}))
date_picker() depends on the time crate for Date. Make sure time is in your Cargo.toml and that you are using it consistently with the version Kael re-exports.
Radio group: radio_group()
radio_group() renders a list of mutually exclusive radio buttons for any Clone + PartialEq type. Like select(), options can be passed as (value, label) tuples.
pub fn radio_group<T, I, O>(id: impl Into<ElementId>, value: T, options: I) -> RadioGroup<T>
where
T: Clone + PartialEq + 'static,
I: IntoIterator<Item = O>,
O: Into<RadioOption<T>>
impl RadioGroup<T> {
pub fn disabled(self) -> Self
pub fn on_change(self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self
}
#[derive(Clone, PartialEq)]
enum Plan { Free, Pro, Enterprise }
radio_group(
"billing-plan",
self.plan.clone(),
[
(Plan::Free, "Free"),
(Plan::Pro, "Pro"),
(Plan::Enterprise, "Enterprise"),
],
)
.on_change(cx.listener(|this, plan: &Plan, _, _| {
this.plan = plan.clone();
}))
Binding patterns with entity state
All form controls in Kael are designed to bind against state stored on a GPUI entity. The standard pattern is:
Declare your form state on a struct
struct ProfileForm {
name: SharedString,
bio: SharedString,
notify: bool,
theme: ThemeChoice,
}
Read state and render controls in Render
impl Render for ProfileForm {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_4()
.child(
text_input("name", self.name.clone())
.placeholder("Display name")
.on_change(cx.listener(|this, text: SharedString, _, _| {
this.name = text;
})),
)
.child(
text_input("bio", self.bio.clone())
.placeholder("Short bio")
.multi_line()
.max_lines(4)
.on_change(cx.listener(|this, text: SharedString, _, _| {
this.bio = text;
})),
)
.child(
checkbox("notify", self.notify)
.label("Email notifications")
.on_change(cx.listener(|this, on: &bool, _, _| {
this.notify = *on;
})),
)
.child(
select(
"theme",
self.theme.clone(),
ThemeChoice::options(),
)
.on_change(cx.listener(|this, choice: &ThemeChoice, _, _| {
this.theme = choice.clone();
})),
)
}
}
Trigger re-render by notifying the context
cx.listener() automatically calls cx.notify() after your closure runs, which schedules a re-render. You do not need to call cx.notify() manually inside on_change closures captured via cx.listener().
Custom renderers
Every form control accepts a render_with() method that hands you a snapshot of the control’s current state and expects an AnyElement in return. Kael keeps all behavior and accessibility — you only supply the visuals.
Custom checkbox
Custom slider
Custom select option
checkbox("agree", self.agreed)
.render_with(|state: CheckboxRenderState, _window, _cx| {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w_4()
.h_4()
.rounded_sm()
.border_1()
.when(state.checked, |el| el.bg(blue()))
)
.when_some(state.label, |el, label| {
el.child(div().text_sm().child(label))
})
.into_any()
})
slider("vol", self.volume)
.min(0.0)
.max(1.0)
.render_with(|state: SliderRenderState, bounds: Bounds<Pixels>, _window, _cx| {
let filled_width = bounds.size.width * state.percentage as f32;
div()
.w(bounds.size.width)
.h_2()
.rounded_full()
.bg(gray())
.child(
div()
.w(px(filled_width))
.h_2()
.rounded_full()
.bg(blue())
)
.into_any()
})
select("lang", self.lang.clone(), languages)
.render_options_with(|state: SelectOptionRenderState<Language>, _window, _cx| {
div()
.flex()
.items_center()
.gap_2()
.when(state.selected, |el| el.font_weight(FontWeight::SEMIBOLD))
.child(icon(state.value.flag_icon()))
.child(div().child(state.label))
.into_any()
})
render_with() replaces the default visual but not the behavior or keyboard handling. Focus, undo/redo, accessibility attributes, and on_change callbacks all continue to work.