Elements
Checkbox
A checkbox selects an option, an item of a list, or all of them. It is checked, unchecked or indeterminate (neither:
a “select all” over a partial selection). It owns the behavior: presses, keyboard activation, the disabled state, and
the checkbox role and its value for screen readers. The look is the app’s: a Checkbox has no box, mark, colors,
sizes or label of its own. It is made of an indicator (the box) with the app’s mark inside it, then its children (a
label), and the app styles each part, with checked:, indeterminate: and disabled: classes that follow the
checkbox.
import { mount, signal, Text, View } from "nsh";
import { Checkbox } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// The parts: geometry in classes (`disabled:` dims the row, following the
// checkbox; `group-checked:` / `group-indeterminate:` show the mark of the
// indicator's state), colors from the palette by the value the app owns;
// the pointer's hover and the keyboard's focus ring from the palette too.
const row = `flex-row items-center gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`;
const indicator = "group size-5 items-center justify-center rounded-md border transition-colors motion-reduce:transition-none";
/** The indicator's surface: the accent while checked or indeterminate. */
const box = (on: boolean) => (on ? ui().accentFill : { ...ui().inset, borderColor: ui().faint });
/** A check while checked, a dash while indeterminate. */
const mark = () => (
<>
<Text class="hidden text-xs font-bold group-checked:flex" style={{ color: ui().onAccent }}>
✓
</Text>
<View class="hidden h-0.5 w-2.5 rounded-full group-indeterminate:flex" style={{ backgroundColor: ui().onAccent }} />
</>
);
const items = [
{ id: "photos", label: "Photos", detail: "2,418 items · 6.1 GB" },
{ id: "documents", label: "Documents", detail: "312 files · 840 MB" },
{ id: "messages", label: "Messages", detail: "Attachments included · 1.2 GB" },
];
function Label(props: { label: string; detail: string; strong?: boolean }) {
return (
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontSize: size.label, fontWeight: props.strong ? "600" : "500", color: ui().text }}>{props.label}</Text>
<Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
</View>
);
}
function App() {
const selected = signal<string[]>(["photos"]);
const events = signal<string[]>([]);
const log = (e: string) => events.set([e, ...events()].slice(0, 3));
const all = () => selected().length === items.length;
const some = () => selected().length > 0 && !all();
const line = () => <View style={{ height: 1, marginLeft: desktop ? 44 : 48, backgroundColor: ui().line }} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>checkbox</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Click a row, or Tab to a checkbox and press Space" : "Tap a row: the label toggles it too"}
</Text>
<Split
gap={14}
inspector={
<>
<Panel title="checked · indeterminate">
<Text style={{ fontSize: 13, color: ui().text }}>
Back up everything: {all() ? "checked" : some() ? "indeterminate" : "unchecked"} · [{selected().join(", ")}]
</Text>
</Panel>
<Log title="onChange(checked)" lines={events()} minHeight={92} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Checkbox
checked={all()}
indeterminate={some()}
onChange={(next) => {
selected.set(next ? items.map((i) => i.id) : []);
log(`Back up everything → ${next}`);
}}
class={row}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
indicatorClass={indicator}
indicatorStyle={box(all() || some())}
indicator={mark()}
>
<Label label="Back up everything" detail={`${selected().length} of ${items.length} selected`} strong />
</Checkbox>
<Each of={items}>
{(item) => (
<>
{line()}
<Checkbox
checked={selected().includes(item().id)}
onChange={(next) => {
selected.set(next ? [...selected(), item().id] : selected().filter((id) => id !== item().id));
log(`${item().label} → ${next}`);
}}
class={row}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
indicatorClass={indicator}
indicatorStyle={box(selected().includes(item().id))}
indicator={mark()}
>
<Label label={item().label} detail={item().detail} />
</Checkbox>
</>
)}
</Each>
</View>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Checkbox checked disabled class={row} indicatorClass={indicator} indicatorStyle={box(true)} indicator={mark()}>
<Label label="System settings" detail="Always backed up" />
</Checkbox>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{checkbox, theme};
use nsh_core::ui::prelude::*;
fn notify() -> impl IntoElement {
let on = signal(false);
checkbox()
.checked(on)
.on_change(move |next: &bool| on.set(*next))
.flex_row()
.items_center()
.gap_2()
.indicator_style(|s| {
s.size(px(20.0))
.items_center()
.justify_center()
.rounded_md()
.border_2()
.border_color(move || theme().border)
.transition_colors()
.checked(|s| s.bg(move || theme().primary).border_color(move || theme().primary))
})
.indicator(text(move || if on.get() { "✓" } else { "" }).text_color(move || theme().primary_foreground))
.child(text("Notify me"))
}import { mount, signal, Text } from "nsh";
import { Checkbox } from "nsh/base";
function App() {
const on = signal(false);
return (
<Checkbox
checked={on()}
onChange={(next) => on.set(next)}
class="flex-row items-center gap-2"
indicatorClass="size-5 items-center justify-center rounded-md border-2 border-border transition-colors checked:border-primary checked:bg-primary"
indicator={<Text class="text-primary-foreground">{on() ? "✓" : ""}</Text>}
>
<Text>Notify me</Text>
</Checkbox>
);
}
mount(App);Checkbox comes from nsh/base, the nsh-base crate in Rust. The value is the app’s: a
press reports the next value with onChange, and the checkbox shows the checked and indeterminate the app passes
back. In Rust, checkbox() makes one with a setter per prop (.checked(..), .indicator_style(..)), a listener per
event (.on_change(..)), the typed style methods for the checkbox itself and .child(..) for its label; the
indicator’s style takes the state methods (.checked(..), .indeterminate(..), .disabled_style(..)) that follow
the checkbox.
#Props
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
checked | .checked(..) | boolean | false | Checked or not. A press reports the next value with onChange; the checkbox shows the value the app passes back. |
indeterminate | .indeterminate(..) | boolean | false | Neither checked nor unchecked, over checked while it holds: screen readers say it is mixed, and a press asks for checked. |
disabled | .disabled(..) | boolean | false | No presses and no press feedback; disabled: styles apply to the checkbox and its indicator, and screen readers say it is dimmed. |
checkedStyle | .checked_style(|s| ..) | StyleProp<ViewStyle> | none | The checkbox’s style while checked and not disabled, as checked: classes. |
indeterminateStyle | .indeterminate_style(|s| ..) | StyleProp<ViewStyle> | none | The checkbox’s style while indeterminate and not disabled, as indeterminate: classes. |
accessibilityLabel | .accessibility_label(..) | string | none | What screen readers say: what the checkbox selects, when its children have no text that says it. |
role | .role(..) | AccessibilityRole | "checkbox" | What it is for screen readers, when it is something more specific (a checked menu item: "menuitem"). |
indicatorClass, indicatorStyle | .indicator_style(|s| ..) | string, StyleProp<ViewStyle> | none | The indicator, the box that shows the value. |
indicator | .indicator(..) | JSX.Element | none | The content of the indicator: the mark (a check, a dash), which the app shows by its value. |
children | .child(..) | JSX.Element | none | The content after the indicator: a label, which screen readers say. |
class, style | style methods | string, StyleProp<ViewStyle> | none | The checkbox itself: the row around the indicator and the label. |
activeStyle, hoverStyle, focusStyle, disabledStyle | .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) | StyleProp<ViewStyle> | none | The checkbox’s style while pressed, hovered, focused or disabled, as the <state>: classes. |
#Events
| Event | Rust | Description |
|---|---|---|
onChange | .on_change(|checked: &bool| ..) | The checkbox was pressed: a tap, a click, Space or Return while it has the keyboard focus, or a screen reader’s activation. It reports the value the checkbox asks for: true from unchecked and from indeterminate, false from checked. |
A disabled checkbox reports nothing.
#Parts
| Part | Inside | Styled with | States |
|---|---|---|---|
| The checkbox | class, style | checked:, indeterminate:, disabled:, active:, hover:, focus: (or checkedStyle, indeterminateStyle, disabledStyle, …) | |
| The indicator | the checkbox, first | indicatorClass, indicatorStyle | checked:, indeterminate:, disabled: follow the checkbox |
| The mark | the indicator | its own elements (indicator) | group-checked:, group-indeterminate: and the other group-*: variants with group on indicatorClass |
| The label | the checkbox, after the indicator | its own elements (children) | group-*: variants with group on the checkbox’s class |
The mark is the app’s content, so it shows what the app decides: a check while checked and a dash while
indeterminate, chosen from the value the app holds, or both marks with group-checked: / group-indeterminate:
classes picking the one shown.
#Indeterminate
A “select all” checkbox is indeterminate while some items are selected. A press asks for checked, and the app selects every item:
const selected = signal<string[]>([]);
const all = () => selected().length === items.length;
const some = () => selected().length > 0 && !all();
<Checkbox
checked={all()}
indeterminate={some()}
onChange={(next) => selected.set(next ? items.map((i) => i.id) : [])}
class="flex-row items-center gap-2"
indicatorClass="group size-5 items-center justify-center rounded-md border-2 border-border checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary"
indicator={
<>
<Text class="hidden text-primary-foreground group-checked:flex">✓</Text>
<View class="hidden h-0.5 w-2.5 bg-primary-foreground group-indeterminate:flex" />
</>
}
>
<Text>Select all</Text>
</Checkbox>
#Disabled
A disabled checkbox takes no presses, and screen readers announce it as dimmed. Its disabled: classes and those of
its indicator apply, over their checked: and indeterminate: ones:
<Checkbox
checked={on()}
disabled={locked()}
onChange={(next) => on.set(next)}
class="flex-row items-center gap-2 disabled:opacity-50"
indicatorClass="size-5 rounded-md border-2 border-border checked:border-primary checked:bg-primary disabled:border-border disabled:bg-muted"
>
<Text>Share usage data</Text>
</Checkbox>
#Accessibility
- A checkbox is one element for screen readers, with the role
"checkbox"(or itsrole) and its value: checked, unchecked, or mixed while indeterminate. The indicator and the mark are parts of it. Its label is the text of its children; giveaccessibilityLabelwhen it has none. disabledis announced as the platform announces it (VoiceOver, TalkBack, UI Automation, the browser’s accessibility tree).- On macOS and the web a checkbox is a stop of the keyboard focus: Tab reaches it, Space and Return toggle it, and
focus:styles show where the focus is.
See Accessibility for labels, roles and announcements in general.
#Platform notes
- iOS, Android and Mac Catalyst: a press is a tap that ends on the checkbox, its label included.
- Desktops (macOS, Windows, Linux, GPUI) and the web: a press is a click. On macOS and the web, Space and Return also toggle the checkbox that has the keyboard focus.
- Every platform draws what the app styles: the indicator, the mark and their transitions.