Elemente
Checkbox
Eine Checkbox wählt eine Option, ein Element einer Liste oder alle aus. Sie ist angehakt, nicht angehakt oder unbestimmt (weder noch: ein „Alle auswählen” bei einer teilweisen Auswahl). Sie übernimmt das Verhalten: Presses, Tastaturaktivierung, den deaktivierten Zustand sowie die Checkbox-Rolle und ihren Wert für Screenreader. Das Aussehen liegt bei der App: Checkbox hat selbst keine Box, kein Häkchen, keine Farben, Größen oder Beschriftung. Sie besteht aus einem Indikator (der Box) mit dem Zeichen der App darin, danach ihren Children (einem Label), und die App gestaltet jeden Teil mit checked:-, indeterminate:- und disabled:-Klassen, die der Checkbox folgen.
import { mount, signal, Text, View } from "nsh";
import { Checkbox } from "nsh/base";
import { CheckMark, desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// The parts: geometry and motion in classes (`disabled:` dims the row,
// following the checkbox; `group-checked:` / `group-indeterminate:` fade in
// the mark of the indicator's state, both kept mounted so each fades out
// too, and a check turning into a dash crosses over), colors from the
// palette by the value the app owns; the pointer's hover and the keyboard's
// focus ring from the palette too. Nothing moves with reduced motion.
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 mark over the box, centered: hidden and a little smaller until its state's `group-*:` classes show it. */
const layer = "absolute inset-0 items-center justify-center opacity-0 scale-75 transition duration-150 ease-out motion-reduce:transition-none";
/** A check while checked, a dash while indeterminate. */
const mark = () => (
<>
<View class={`${layer} group-checked:opacity-100 group-checked:scale-100`}>
<CheckMark size={14} weight={2.5} tint={ui().onAccent} />
</View>
<View class={`${layer} group-indeterminate:opacity-100 group-indeterminate:scale-100`}>
<View class="h-0.5 w-2.5 rounded-full" style={{ backgroundColor: ui().onAccent }} />
</View>
</>
);
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={({ checked }) => {
selected.set(checked ? items.map((i) => i.id) : []);
log(`Back up everything → ${checked}`);
}}
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={({ checked }) => {
selected.set(checked ? [...selected(), item().id] : selected().filter((id) => id !== item().id));
log(`${item().label} → ${checked}`);
}}
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::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={({ checked }) => on.set(checked)}
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 stammt aus nsh/base, dem nsh-base-Crate in Rust. Der Wert gehört der App: Ein Press meldet den nächsten Wert mit onChange, und die Checkbox zeigt die Werte checked und indeterminate, die die App zurückgibt. In Rust erzeugt checkbox() eine Checkbox mit einem Setter pro Prop (.checked(..), .indicator_style(..)), einem Listener pro Event (.on_change(..)), den typisierten Style-Methoden für die Checkbox selbst und .child(..) für ihr Label; der Style des Indikators nimmt die Zustandsmethoden (.checked(..), .indeterminate(..), .disabled_style(..)) entgegen, die der Checkbox folgen.
#Props
| Prop | Rust | Typ | Standard | Beschreibung |
|---|---|---|---|---|
checked |
.checked(..) |
boolean |
false |
Angehakt oder nicht. Ein Press meldet den nächsten Wert mit onChange; die Checkbox zeigt den Wert, den die App zurückgibt. |
indeterminate |
.indeterminate(..) |
boolean |
false |
Weder angehakt noch nicht angehakt, und zwar vor checked, solange er gilt: Screenreader sagen, dass sie gemischt ist, und ein Press fordert „angehakt” an. |
disabled |
.disabled(..) |
boolean |
false |
Keine Presses und kein Press-Feedback; disabled:-Styles gelten für die Checkbox und ihren Indikator, und Screenreader sagen, dass sie abgeblendet ist. |
checkedStyle |
.checked_style(|s| ..) |
StyleProp<ViewStyle> |
keiner | Der Style der Checkbox, solange sie angehakt und nicht deaktiviert ist, als checked:-Klassen. |
indeterminateStyle |
.indeterminate_style(|s| ..) |
StyleProp<ViewStyle> |
keiner | Der Style der Checkbox, solange sie unbestimmt und nicht deaktiviert ist, als indeterminate:-Klassen. |
accessibilityLabel |
.accessibility_label(..) |
string |
keiner | Was Screenreader sagen: was die Checkbox auswählt, wenn ihre Children keinen Text enthalten, der es sagt. |
role |
.role(..) |
AccessibilityRole |
"checkbox" |
Was sie für Screenreader ist, wenn es etwas Spezifischeres ist (ein angehakter Menüeintrag: "menuitem"). |
indicatorClass, indicatorStyle |
.indicator_style(|s| ..) |
string, StyleProp<ViewStyle> |
keiner | Der Indikator, die Box, die den Wert zeigt. |
indicator |
.indicator(..) |
JSX.Element |
keiner | Der Inhalt des Indikators: das Zeichen (ein Häkchen, ein Strich), das die App abhängig von ihrem Wert zeigt. |
hitSlop |
.hit_slop(..) |
number | Partial<Edges> |
keiner | Wie weit außerhalb ihrer Grenzen Presses noch zählen, in Punkten: eine Zahl für alle Seiten oder die benannten Seiten. Wie hitSlop einer View. |
children |
.child(..) |
JSX.Element |
keiner | Der Inhalt nach dem Indikator: ein Label, das Screenreader vorlesen. |
class, style |
Style-Methoden | string, StyleProp<ViewStyle> |
keiner | Die Checkbox selbst: die Zeile um den Indikator und das Label. |
activeStyle, hoverStyle, focusStyle, disabledStyle |
.active(|s| ..), .hover(..), .focus(..), .disabled_style(..) |
StyleProp<ViewStyle> |
keiner | Der Style der Checkbox, solange sie gedrückt, gehovert, fokussiert oder deaktiviert ist, als <state>:-Klassen. |
#Events
| Event | Rust | Beschreibung |
|---|---|---|
onChange |
.on_change(|checked: &bool| ..) |
Die Checkbox wurde gedrückt: ein Tap, ein Klick, Space oder Return, während sie den Tastaturfokus hat, oder die Aktivierung durch einen Screenreader. Sie meldet den Wert, den die Checkbox anfordert, als checked: true von „nicht angehakt” und von „unbestimmt” aus, false von „angehakt” aus. |
Eine deaktivierte Checkbox meldet nichts.
#Teile
| Teil | Enthalten in | Gestaltet mit | Zustände |
|---|---|---|---|
| Die Checkbox | class, style |
checked:, indeterminate:, disabled:, active:, hover:, focus: (oder checkedStyle, indeterminateStyle, disabledStyle, …) |
|
| Der Indikator | der Checkbox, als Erstes | indicatorClass, indicatorStyle |
checked:, indeterminate:, disabled: folgen der Checkbox |
| Das Zeichen | dem Indikator | eigenen Elementen (indicator) |
group-checked:, group-indeterminate: und die anderen group-*:-Varianten mit group auf indicatorClass |
| Das Label | der Checkbox, nach dem Indikator | eigenen Elementen (children) |
group-*:-Varianten mit group auf dem class der Checkbox |
Das Zeichen ist Inhalt der App, es zeigt also, was die App festlegt: ein Häkchen, solange sie angehakt ist, und einen Strich, solange sie unbestimmt ist, gewählt anhand des Werts, den die App hält, oder beide Zeichen, wobei group-checked:- / group-indeterminate:-Klassen das angezeigte auswählen.
#Unbestimmt
Eine „Alle auswählen”-Checkbox ist unbestimmt, solange einige Elemente ausgewählt sind. Ein Press fordert „angehakt” an, und die App wählt jedes Element aus:
const selected = signal<string[]>([]);
const all = () => selected().length === items.length;
const some = () => selected().length > 0 && !all();
<Checkbox
checked={all()}
indeterminate={some()}
onChange={({ checked }) => selected.set(checked ? 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>
#Deaktiviert
Eine deaktivierte Checkbox nimmt keine Presses entgegen, und Screenreader kündigen sie als abgeblendet an. Ihre disabled:-Klassen und die ihres Indikators gelten und überschreiben deren checked:- und indeterminate:-Klassen:
<Checkbox
checked={on()}
disabled={locked()}
onChange={({ checked }) => on.set(checked)}
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>
#Ein kleines Ziel mit hitSlop
Eine Checkbox ohne Label daneben (das Auswahlkästchen einer Zeile) kann kleiner als ein Finger sein. hitSlop lässt Presses zählen, die in ihrer Nähe landen, in Punkten über jede Seite hinaus, ohne ihr Layout oder ihr Aussehen zu ändern: eine Zahl für alle Seiten oder die benannten Seiten ({ top: 8, bottom: 8 }), so wie hitSlop einer View sie entgegennimmt.
<Checkbox
checked={selected()}
hitSlop={12}
accessibilityLabel="Select row"
onChange={({ checked }) => selected.set(checked)}
indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>
#Barrierefreiheit
- Eine Checkbox ist für Screenreader ein einzelnes Element mit der Rolle
"checkbox"(oder ihremrole) und ihrem Wert: angehakt, nicht angehakt oder gemischt, solange sie unbestimmt ist. Der Indikator und das Zeichen sind Teile davon. Ihr Label ist der Text ihrer Children; gibaccessibilityLabelan, wenn sie keinen hat. disabledwird so angekündigt, wie die Plattform es ankündigt (VoiceOver, TalkBack, UI Automation, der Accessibility Tree des Browsers).- Unter macOS und im Web ist eine Checkbox ein Haltepunkt des Tastaturfokus: Tab erreicht sie, Space und Return schalten sie um, und
focus:-Styles zeigen, wo der Fokus liegt.
Siehe Barrierefreiheit für Labels, Rollen und Ankündigungen im Allgemeinen.
#Plattformhinweise
- iOS, Android und Mac Catalyst: Ein Press ist ein Tap, der auf der Checkbox endet, ihr Label eingeschlossen.
- Desktops (macOS, Windows, Linux, GPUI) und das Web: Ein Press ist ein Klick. Unter macOS und im Web schalten Space und Return außerdem die Checkbox um, die den Tastaturfokus hat.
- Jede Plattform zeichnet, was die App gestaltet: den Indikator, das Zeichen und ihre Übergänge.