Elementos
Checkbox
Un checkbox selecciona una opción, un elemento de una lista o todos. Está marcado, desmarcado o indeterminado (ni lo uno ni lo otro: un «seleccionar todo» sobre una selección parcial). Se encarga del comportamiento: pulsaciones, activación por teclado, el estado deshabilitado, y el rol de checkbox y su valor para los lectores de pantalla. El aspecto es de la app: un Checkbox no tiene caja, marca, colores, tamaños ni etiqueta propios. Se compone de un indicador (la caja) con la marca de la app dentro, y después sus hijos (una etiqueta), y la app da estilo a cada parte, con clases checked:, indeterminate: y disabled: que siguen al 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 proviene de nsh/base, el crate nsh-base de Rust. El valor es de la app: una pulsación informa del siguiente valor con onChange, y el checkbox muestra el checked y indeterminate que la app le devuelve. En Rust, checkbox() crea uno con un setter por prop (.checked(..), .indicator_style(..)), un listener por evento (.on_change(..)), los métodos de estilo tipados para el propio checkbox y .child(..) para su etiqueta; el estilo del indicador admite los métodos de estado (.checked(..), .indeterminate(..), .disabled_style(..)) que siguen al checkbox.
#Props
| Prop | Rust | Tipo | Valor por defecto | Descripción |
|---|---|---|---|---|
checked |
.checked(..) |
boolean |
false |
Marcado o no. Una pulsación informa del siguiente valor con onChange; el checkbox muestra el valor que la app le devuelve. |
indeterminate |
.indeterminate(..) |
boolean |
false |
Ni marcado ni desmarcado, por encima de checked mientras se mantiene: los lectores de pantalla dicen que es mixto, y una pulsación pide marcado. |
disabled |
.disabled(..) |
boolean |
false |
Sin pulsaciones ni respuesta a la pulsación; los estilos disabled: se aplican al checkbox y a su indicador, y los lectores de pantalla dicen que está atenuado. |
checkedStyle |
.checked_style(|s| ..) |
StyleProp<ViewStyle> |
ninguno | El estilo del checkbox mientras está marcado y no deshabilitado, como clases checked:. |
indeterminateStyle |
.indeterminate_style(|s| ..) |
StyleProp<ViewStyle> |
ninguno | El estilo del checkbox mientras es indeterminado y no deshabilitado, como clases indeterminate:. |
accessibilityLabel |
.accessibility_label(..) |
string |
ninguno | Lo que dicen los lectores de pantalla: qué selecciona el checkbox, cuando sus hijos no tienen texto que lo diga. |
role |
.role(..) |
AccessibilityRole |
"checkbox" |
Qué es para los lectores de pantalla, cuando es algo más específico (un elemento de menú marcado: "menuitem"). |
indicatorClass, indicatorStyle |
.indicator_style(|s| ..) |
string, StyleProp<ViewStyle> |
ninguno | El indicador, la caja que muestra el valor. |
indicator |
.indicator(..) |
JSX.Element |
ninguno | El contenido del indicador: la marca (una marca de verificación, un guion), que la app muestra según su valor. |
hitSlop |
.hit_slop(..) |
number | Partial<Edges> |
ninguno | Hasta qué distancia fuera de sus límites siguen contando las pulsaciones, en puntos: un número para todos los lados, o los lados con nombre. Como el hitSlop de una vista. |
children |
.child(..) |
JSX.Element |
ninguno | El contenido tras el indicador: una etiqueta, que los lectores de pantalla leen. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | El propio checkbox: la fila que rodea al indicador y a la etiqueta. |
activeStyle, hoverStyle, focusStyle, disabledStyle |
.active(|s| ..), .hover(..), .focus(..), .disabled_style(..) |
StyleProp<ViewStyle> |
ninguno | El estilo del checkbox mientras está pulsado, con hover, con foco o deshabilitado, como las clases <state>:. |
#Eventos
| Evento | Rust | Descripción |
|---|---|---|
onChange |
.on_change(|checked: &bool| ..) |
El checkbox fue pulsado: un toque, un clic, Espacio o Return mientras tiene el foco del teclado, o la activación de un lector de pantalla. Informa del valor que el checkbox pide como checked: true desde desmarcado y desde indeterminado, false desde marcado. |
Un checkbox deshabilitado no informa de nada.
#Partes
| Parte | Dentro de | Estilo con | Estados |
|---|---|---|---|
| El checkbox | class, style |
checked:, indeterminate:, disabled:, active:, hover:, focus: (o checkedStyle, indeterminateStyle, disabledStyle, …) |
|
| El indicador | el checkbox, primero | indicatorClass, indicatorStyle |
checked:, indeterminate:, disabled: siguen al checkbox |
| La marca | el indicador | sus propios elementos (indicator) |
group-checked:, group-indeterminate: y las demás variantes group-*: con group sobre indicatorClass |
| La etiqueta | el checkbox, tras el indicador | sus propios elementos (children) |
variantes group-*: con group sobre el class del checkbox |
La marca es contenido de la app, así que muestra lo que la app decida: una marca de verificación mientras está marcado y un guion mientras es indeterminado, elegidos según el valor que la app mantiene, o ambas marcas con clases group-checked: / group-indeterminate: que eligen la que se muestra.
#Indeterminado
Un checkbox «seleccionar todo» es indeterminado mientras hay algunos elementos seleccionados. Una pulsación pide marcado, y la app selecciona todos los elementos:
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>
#Deshabilitado
Un checkbox deshabilitado no admite pulsaciones, y los lectores de pantalla lo anuncian como atenuado. Sus clases disabled: y las de su indicador se aplican, por encima de las checked: y 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>
#Un objetivo pequeño con hitSlop
Un checkbox sin etiqueta a su lado (la caja de selección de una fila) puede ser más pequeño que un dedo. hitSlop hace que cuenten las pulsaciones que caen cerca, en puntos más allá de cada lado, sin cambiar su disposición ni su aspecto: un número para todos los lados, o los lados con nombre ({ top: 8, bottom: 8 }), como los toma el hitSlop de una vista.
<Checkbox
checked={selected()}
hitSlop={12}
accessibilityLabel="Select row"
onChange={({ checked }) => selected.set(checked)}
indicatorClass="size-4 rounded border border-border checked:bg-primary"
/>
#Accesibilidad
- Un checkbox es un único elemento para los lectores de pantalla, con el rol
"checkbox"(o surole) y su valor: marcado, desmarcado, o mixto mientras es indeterminado. El indicador y la marca son partes de él. Su etiqueta es el texto de sus hijos; indicaaccessibilityLabelcuando no tiene ninguno. disabledse anuncia como lo anuncia la plataforma (VoiceOver, TalkBack, UI Automation, el árbol de accesibilidad del navegador).- En macOS y la web un checkbox es una parada del foco del teclado: Tab llega hasta él, Espacio y Return lo alternan, y los estilos
focus:muestran dónde está el foco.
Consulta Accesibilidad para etiquetas, roles y anuncios en general.
#Notas de plataforma
- iOS, Android y Mac Catalyst: una pulsación es un toque que termina sobre el checkbox, etiqueta incluida.
- Escritorio (macOS, Windows, Linux, GPUI) y la web: una pulsación es un clic. En macOS y la web, Espacio y Return también alternan el checkbox que tiene el foco del teclado.
- Cada plataforma dibuja lo que la app estiliza: el indicador, la marca y sus transiciones.