Skip to main content

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:
1

Declare your form state on a struct

struct ProfileForm {
    name: SharedString,
    bio: SharedString,
    notify: bool,
    theme: ThemeChoice,
}
2

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();
                })),
            )
    }
}
3

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.
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()
    })
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.

Build docs developers (and LLMs) love