Elementos
Checkbox
Um checkbox seleciona uma opção, um item de uma lista ou todos eles. Ele está marcado, desmarcado ou indeterminado (nem um nem outro: um “selecionar tudo” sobre uma seleção parcial). Ele é responsável pelo comportamento: pressionamentos, ativação pelo teclado, o estado desabilitado, e o papel de checkbox e seu valor para leitores de tela. A aparência é do app: um Checkbox não tem caixa, marca, cores, tamanhos nem rótulo próprios. Ele é composto por um indicador (a caixa) com a marca do app dentro, depois seus filhos (um rótulo), e o app estiliza cada parte, com classes checked:, indeterminate: e disabled: que acompanham o 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 vem de nsh/base, o crate nsh-base em Rust. O valor é do app: um pressionamento informa o próximo valor com onChange, e o checkbox exibe o checked e o indeterminate que o app devolve. Em Rust, checkbox() cria um, com um setter por prop (.checked(..), .indicator_style(..)), um listener por evento (.on_change(..)), os métodos de estilo tipados para o próprio checkbox e .child(..) para seu rótulo; o estilo do indicador recebe os métodos de estado (.checked(..), .indeterminate(..), .disabled_style(..)) que acompanham o checkbox.
#Props
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
checked |
.checked(..) |
boolean |
false |
Marcado ou não. Um pressionamento informa o próximo valor com onChange; o checkbox exibe o valor que o app devolve. |
indeterminate |
.indeterminate(..) |
boolean |
false |
Nem marcado nem desmarcado, sobrepondo checked enquanto estiver ativo: leitores de tela dizem que está misto, e um pressionamento pede marcado. |
disabled |
.disabled(..) |
boolean |
false |
Sem pressionamentos e sem feedback de pressionamento; os estilos disabled: se aplicam ao checkbox e ao seu indicador, e leitores de tela dizem que está esmaecido. |
checkedStyle |
.checked_style(|s| ..) |
StyleProp<ViewStyle> |
nenhum | O estilo do checkbox enquanto está marcado e não desabilitado, como classes checked:. |
indeterminateStyle |
.indeterminate_style(|s| ..) |
StyleProp<ViewStyle> |
nenhum | O estilo do checkbox enquanto está indeterminado e não desabilitado, como classes indeterminate:. |
accessibilityLabel |
.accessibility_label(..) |
string |
nenhum | O que os leitores de tela dizem: o que o checkbox seleciona, quando seus filhos não têm texto que o diga. |
role |
.role(..) |
AccessibilityRole |
"checkbox" |
O que ele é para os leitores de tela, quando é algo mais específico (um item de menu marcado: "menuitem"). |
indicatorClass, indicatorStyle |
.indicator_style(|s| ..) |
string, StyleProp<ViewStyle> |
nenhum | O indicador, a caixa que mostra o valor. |
indicator |
.indicator(..) |
JSX.Element |
nenhum | O conteúdo do indicador: a marca (um check, um traço), que o app exibe conforme o valor. |
hitSlop |
.hit_slop(..) |
number | Partial<Edges> |
nenhum | Até onde, fora de seus limites, os pressionamentos ainda contam, em pontos: um número para todos os lados, ou os lados nomeados. Como o hitSlop de uma view. |
children |
.child(..) |
JSX.Element |
nenhum | O conteúdo depois do indicador: um rótulo, que os leitores de tela leem. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | O próprio checkbox: a linha em torno do indicador e do rótulo. |
activeStyle, hoverStyle, focusStyle, disabledStyle |
.active(|s| ..), .hover(..), .focus(..), .disabled_style(..) |
StyleProp<ViewStyle> |
nenhum | O estilo do checkbox enquanto está pressionado, com hover, com foco ou desabilitado, como as classes <state>:. |
#Eventos
| Evento | Rust | Descrição |
|---|---|---|
onChange |
.on_change(|checked: &bool| ..) |
O checkbox foi pressionado: um toque, um clique, Space ou Return enquanto ele tem o foco do teclado, ou uma ativação por leitor de tela. Informa o valor que o checkbox pede como checked: true a partir de desmarcado e de indeterminado, false a partir de marcado. |
Um checkbox desabilitado não informa nada.
#Partes
| Parte | Dentro de | Estilizada com | Estados |
|---|---|---|---|
| O checkbox | class, style |
checked:, indeterminate:, disabled:, active:, hover:, focus: (ou checkedStyle, indeterminateStyle, disabledStyle, …) |
|
| O indicador | o checkbox, primeiro | indicatorClass, indicatorStyle |
checked:, indeterminate:, disabled: acompanham o checkbox |
| A marca | o indicador | seus próprios elementos (indicator) |
group-checked:, group-indeterminate: e as outras variantes group-*: com group em indicatorClass |
| O rótulo | o checkbox, depois do indicador | seus próprios elementos (children) |
variantes group-*: com group no class do checkbox |
A marca é conteúdo do app, então ela mostra o que o app decidir: um check enquanto marcado e um traço enquanto indeterminado, escolhidos a partir do valor que o app mantém, ou as duas marcas, com classes group-checked: / group-indeterminate: escolhendo a que aparece.
#Indeterminado
Um checkbox “selecionar tudo” fica indeterminado enquanto alguns itens estão selecionados. Um pressionamento pede marcado, e o app seleciona todos os itens:
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>
#Desabilitado
Um checkbox desabilitado não recebe pressionamentos, e os leitores de tela o anunciam como esmaecido. Suas classes disabled: e as do seu indicador se aplicam, sobrepondo as checked: e 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>
#Um alvo pequeno com hitSlop
Um checkbox sem rótulo ao lado (a caixa de seleção de uma linha) pode ser menor que um dedo. hitSlop faz com que pressionamentos que caiam perto dele contem, em pontos além de cada lado, sem alterar seu layout nem sua aparência: um número para todos os lados, ou os lados nomeados ({ top: 8, bottom: 8 }), como o hitSlop de uma view os recebe.
<Checkbox
checked={selected()}
hitSlop={12}
accessibilityLabel="Select row"
onChange={({ checked }) => selected.set(checked)}
indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>
#Acessibilidade
- Um checkbox é um único elemento para leitores de tela, com o papel
"checkbox"(ou seurole) e seu valor: marcado, desmarcado ou misto enquanto indeterminado. O indicador e a marca são partes dele. Seu rótulo é o texto de seus filhos; informeaccessibilityLabelquando não houver nenhum. disabledé anunciado como a plataforma o anuncia (VoiceOver, TalkBack, UI Automation, a árvore de acessibilidade do navegador).- No macOS e na web, um checkbox é uma parada do foco do teclado: Tab chega até ele, Space e Return o alternam, e os estilos
focus:mostram onde está o foco.
Veja Acessibilidade para rótulos, papéis e anúncios em geral.
#Notas de plataforma
- iOS, Android e Mac Catalyst: um pressionamento é um toque que termina no checkbox, incluindo seu rótulo.
- Desktops (macOS, Windows, Linux, GPUI) e a web: um pressionamento é um clique. No macOS e na web, Space e Return também alternam o checkbox que tem o foco do teclado.
- Todas as plataformas desenham o que o app estiliza: o indicador, a marca e suas transições.