Éléments
Checkbox
Une checkbox sélectionne une option, un élément d’une liste, ou tous. Elle est cochée, décochée ou indéterminée (ni l’un ni l’autre : un « tout sélectionner » sur une sélection partielle). Elle porte le comportement : les pressions, l’activation au clavier, l’état désactivé, ainsi que le rôle checkbox et sa valeur pour les lecteurs d’écran. L’apparence appartient à l’app : un Checkbox n’a ni case, ni coche, ni couleurs, ni tailles, ni libellé propres. Il est composé d’un indicateur (la case) contenant la coche de l’app, puis de ses enfants (un libellé), et l’app style chaque partie, avec des classes checked:, indeterminate: et disabled: qui suivent la checkbox.
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 vient de nsh/base, le crate nsh-base de Rust. La valeur appartient à l’app : une pression signale la valeur suivante avec onChange, et la checkbox affiche les checked et indeterminate que l’app lui renvoie. En Rust, checkbox() en construit une avec un setter par prop (.checked(..), .indicator_style(..)), un écouteur par événement (.on_change(..)), les méthodes de style typées pour la checkbox elle-même et .child(..) pour son libellé ; le style de l’indicateur accepte les méthodes d’état (.checked(..), .indeterminate(..), .disabled_style(..)) qui suivent la checkbox.
#Props
| Prop | Rust | Type | Défaut | Description |
|---|---|---|---|---|
checked | .checked(..) | boolean | false | Cochée ou non. Une pression signale la valeur suivante avec onChange ; la checkbox affiche la valeur que l’app lui renvoie. |
indeterminate | .indeterminate(..) | boolean | false | Ni cochée ni décochée, par-dessus checked tant qu’elle est vraie : les lecteurs d’écran indiquent qu’elle est mixte, et une pression demande l’état coché. |
disabled | .disabled(..) | boolean | false | Aucune pression et aucun retour de pression ; les styles disabled: s’appliquent à la checkbox et à son indicateur, et les lecteurs d’écran l’annoncent comme grisée. |
checkedStyle | .checked_style(|s| ..) | StyleProp<ViewStyle> | aucun | Le style de la checkbox lorsqu’elle est cochée et non désactivée, sous forme de classes checked:. |
indeterminateStyle | .indeterminate_style(|s| ..) | StyleProp<ViewStyle> | aucun | Le style de la checkbox lorsqu’elle est indéterminée et non désactivée, sous forme de classes indeterminate:. |
accessibilityLabel | .accessibility_label(..) | string | aucun | Ce que disent les lecteurs d’écran : ce que la checkbox sélectionne, lorsque ses enfants n’ont pas de texte qui le dise. |
role | .role(..) | AccessibilityRole | "checkbox" | Ce que c’est pour les lecteurs d’écran, lorsque c’est quelque chose de plus précis (un élément de menu coché : "menuitem"). |
indicatorClass, indicatorStyle | .indicator_style(|s| ..) | string, StyleProp<ViewStyle> | aucun | L’indicateur, la case qui montre la valeur. |
indicator | .indicator(..) | JSX.Element | aucun | Le contenu de l’indicateur : la coche (une coche, un tiret), que l’app affiche selon sa valeur. |
hitSlop | .hit_slop(..) | number | Partial<Edges> | aucun | Jusqu’où, en dehors de ses limites, les pressions comptent encore, en points : un nombre pour tous les côtés, ou les côtés nommés. Comme le hitSlop d’une vue. |
children | .child(..) | JSX.Element | aucun | Le contenu après l’indicateur : un libellé, que les lecteurs d’écran énoncent. |
class, style | méthodes de style | string, StyleProp<ViewStyle> | aucun | La checkbox elle-même : la ligne autour de l’indicateur et du libellé. |
activeStyle, hoverStyle, focusStyle, disabledStyle | .active(|s| ..), .hover(..), .focus(..), .disabled_style(..) | StyleProp<ViewStyle> | aucun | Le style de la checkbox lorsqu’elle est pressée, survolée, qu’elle a le focus ou qu’elle est désactivée, sous forme de classes <state>:. |
#Événements
| Événement | Rust | Description |
|---|---|---|
onChange | .on_change(|checked: &bool| ..) | La checkbox a été pressée : un toucher, un clic, Espace ou Entrée lorsqu’elle a le focus clavier, ou l’activation par un lecteur d’écran. Elle signale la valeur que la checkbox demande sous la forme checked : true depuis décochée et depuis indéterminée, false depuis cochée. |
Une checkbox désactivée ne signale rien.
#Parties
| Partie | À l’intérieur de | Stylée avec | États |
|---|---|---|---|
| La checkbox | class, style | checked:, indeterminate:, disabled:, active:, hover:, focus: (ou checkedStyle, indeterminateStyle, disabledStyle, …) | |
| L’indicateur | la checkbox, en premier | indicatorClass, indicatorStyle | checked:, indeterminate:, disabled: suivent la checkbox |
| La coche | l’indicateur | ses propres éléments (indicator) | group-checked:, group-indeterminate: et les autres variantes group-*: avec group sur indicatorClass |
| Le libellé | la checkbox, après l’indicateur | ses propres éléments (children) | variantes group-*: avec group sur le class de la checkbox |
La coche est le contenu de l’app : elle montre donc ce que l’app décide. Une coche lorsque la checkbox est cochée et un tiret lorsqu’elle est indéterminée, choisis d’après la valeur que l’app détient, ou bien les deux marques avec des classes group-checked: / group-indeterminate: qui choisissent celle qui est affichée.
#Indéterminé
Une checkbox « tout sélectionner » est indéterminée tant que certains éléments sont sélectionnés. Une pression demande l’état coché, et l’app sélectionne tous les éléments :
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>
#Désactivée
Une checkbox désactivée ne prend aucune pression, et les lecteurs d’écran l’annoncent comme grisée. Ses classes disabled: et celles de son indicateur s’appliquent, par-dessus leurs classes checked: et indeterminate: :
<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>
#Une petite cible avec hitSlop
Une checkbox sans libellé à côté (la case de sélection d’une ligne) peut être plus petite qu’un doigt. hitSlop fait compter les pressions qui tombent près d’elle, en points au-delà de chaque côté, sans changer sa mise en page ni son apparence : un nombre pour tous les côtés, ou les côtés nommés ({ top: 8, bottom: 8 }), comme le prend le hitSlop d’une vue.
<Checkbox
checked={selected()}
hitSlop={12}
accessibilityLabel="Select row"
onChange={({ checked }) => selected.set(checked)}
indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>
#Accessibilité
- Une checkbox est un seul élément pour les lecteurs d’écran, avec le rôle
"checkbox"(ou sonrole) et sa valeur : cochée, décochée, ou mixte lorsqu’elle est indéterminée. L’indicateur et la coche en font partie. Son libellé est le texte de ses enfants ; donnezaccessibilityLabellorsqu’elle n’en a pas. disabledest annoncé comme la plateforme l’annonce (VoiceOver, TalkBack, UI Automation, l’arbre d’accessibilité du navigateur).- Sur macOS et le web, une checkbox est un arrêt du focus clavier : Tab l’atteint, Espace et Entrée la basculent, et les styles
focus:montrent où est le focus.
Voir Accessibilité pour les libellés, rôles et annonces en général.
#Notes sur les plateformes
- iOS, Android et Mac Catalyst : une pression est un toucher qui se termine sur la checkbox, libellé compris.
- Ordinateurs (macOS, Windows, Linux, GPUI) et le web : une pression est un clic. Sur macOS et le web, Espace et Entrée basculent aussi la checkbox qui a le focus clavier.
- Chaque plateforme dessine ce que l’app style : l’indicateur, la coche et leurs transitions.