Elementos
Switch
A switch liga ou desliga uma configuração, e a mudança tem efeito imediato. Ele cuida do comportamento: pressionamentos, ativação pelo teclado, o estado desabilitado, e o papel de switch e seu valor para leitores de tela. A aparência é do app: um Switch não tem cores, tamanhos, movimento nem rótulo próprios. Ele é composto por um track com um thumb dentro dele, depois seus filhos (um rótulo), e o app estiliza cada parte, com classes checked: e disabled: que acompanham o switch.
import { mount, signal, Text, View } from "nsh";
import { Switch } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// The parts: geometry and motion in classes (`checked:` moves the thumb,
// `disabled:` dims the row, both following the switch), colors from the
// palette by the value the app owns. A pointer gets the compact size; the
// pointer's hover and the keyboard's focus ring come from the palette, and
// the thumb and track stay still with reduced motion.
const row = `flex-row-reverse items-center justify-between gap-3 ${desktop ? "px-3 py-2" : "px-4 py-3"} active:opacity-70 disabled:opacity-40`;
const track = `${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`;
const thumb = desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5";
function Setting(props: { label: string; detail: string; checked: boolean; disabled?: boolean; onChange: (e: { checked: boolean }) => void }) {
return (
<Switch
checked={props.checked}
disabled={props.disabled}
onChange={props.onChange}
class={row}
hoverStyle={props.disabled ? undefined : hoverFill()}
focusStyle={focusRing()}
trackClass={track}
trackStyle={props.checked ? ui().accentFill : ui().inset}
thumbClass={thumb}
thumbStyle={props.checked ? { backgroundColor: ui().onAccent } : ui().raised}
>
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
<Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
</View>
</Switch>
);
}
function App() {
const wifi = signal(true);
const bluetooth = signal(false);
const ask = signal(true);
const events = signal<string[]>([]);
const change = (name: string, value: { set: (v: boolean) => void }) => ({ checked }: { checked: boolean }) => {
value.set(checked);
events.set([`${name} → ${checked}`, ...events()].slice(0, 3));
};
const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, 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 }}>switch</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Click a row, or Tab to a switch and press Space" : "Tap a row: the label toggles it too"}
</Text>
<Split
gap={14}
inspector={
<>
<Panel title="checked · disabled">
<Text style={{ fontSize: 13, color: ui().text }}>
Wi-Fi {wifi() ? "on" : "off"} · ask {ask() ? "on" : "off"}
{wifi() ? "" : " (disabled)"} · Bluetooth {bluetooth() ? "on" : "off"}
</Text>
</Panel>
<Log title="onChange({ checked })" lines={events()} minHeight={92} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Setting label="Wi-Fi" detail={wifi() ? "Connected to Studio" : "Off"} checked={wifi()} onChange={change("Wi-Fi", wifi)} />
{line()}
<Setting
label="Ask to join networks"
detail={wifi() ? "Known networks join on their own" : "Turn on Wi-Fi first"}
checked={ask()}
disabled={!wifi()}
onChange={change("Ask to join", ask)}
/>
{line()}
<Setting label="Bluetooth" detail={bluetooth() ? "Discoverable as “Desk”" : "Off"} checked={bluetooth()} onChange={change("Bluetooth", bluetooth)} />
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{switch, theme};
use nsh_core::prelude::*;
fn wifi() -> impl IntoElement {
let on = signal(false);
switch()
.checked(on)
.on_change(move |next: &bool| on.set(*next))
.flex_row()
.items_center()
.gap_3()
.track_style(|s| {
s.w(px(44.0))
.p(px(2.0))
.rounded_full()
.bg(move || theme().muted)
.transition_colors()
.checked(|s| s.bg(move || theme().primary))
})
.thumb_style(|s| {
s.size(px(20.0))
.rounded_full()
.bg(move || theme().background)
.shadow_sm()
.transition_transform()
.checked(|s| s.translate_x(px(20.0)))
})
.child(text("Wi-Fi"))
}import { mount, signal, Text } from "nsh";
import { Switch } from "nsh/base";
function App() {
const on = signal(false);
return (
<Switch
checked={on()}
onChange={({ checked }) => on.set(checked)}
class="flex-row items-center gap-3"
trackClass="w-11 rounded-full bg-muted p-0.5 transition-colors checked:bg-primary"
thumbClass="size-5 rounded-full bg-background shadow-sm transition-transform checked:translate-x-5"
>
<Text>Wi-Fi</Text>
</Switch>
);
}
mount(App);Switch vem de nsh/base, o crate nsh-base do Rust. O valor é do app: um pressionamento informa o próximo valor com onChange, e o switch mostra o checked que o app devolve. Em Rust, switch() cria um, com um setter por prop (.checked(..), .track_style(..)), um listener por evento (.on_change(..)), os métodos de estilo tipados para o próprio switch e .child(..) para o rótulo; o estilo de uma parte aceita os métodos de estado (.checked(..), .disabled_style(..)) que acompanham o switch.
#Props
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
checked |
.checked(..) |
boolean |
false |
Ligado ou desligado. Um pressionamento informa o próximo valor com onChange; o switch mostra o valor que o app devolve. |
disabled |
.disabled(..) |
boolean |
false |
Sem pressionamentos e sem feedback de pressionamento; os estilos disabled: se aplicam ao switch e às suas partes, e os leitores de tela dizem que ele está esmaecido. |
checkedStyle |
.checked_style(|s| ..) |
StyleProp<ViewStyle> |
nenhum | O estilo do switch enquanto checked e não desabilitado, como classes checked:. |
accessibilityLabel |
.accessibility_label(..) |
string |
nenhum | O que os leitores de tela dizem: o que o switch liga, quando seus filhos não têm texto que diga isso. |
trackClass, trackStyle |
.track_style(|s| ..) |
string, StyleProp<ViewStyle> |
nenhum | O track, a parte ao longo da qual o thumb se move. |
thumbClass, thumbStyle |
.thumb_style(|s| ..) |
string, StyleProp<ViewStyle> |
nenhum | O thumb, dentro do track. |
hitSlop |
.hit_slop(..) |
number | Partial<Edges> |
nenhum | Até que distância 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 track: um rótulo, que os leitores de tela leem. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
nenhum | O próprio switch: a linha em torno do track e do rótulo. |
activeStyle, hoverStyle, focusStyle, disabledStyle |
.active(|s| ..), .hover(..), .focus(..), .disabled_style(..) |
StyleProp<ViewStyle> |
nenhum | O estilo do switch enquanto pressionado, com hover, com foco ou desabilitado, como as classes <state>:. |
#Eventos
| Evento | Rust | Descrição |
|---|---|---|
onChange |
.on_change(|checked: &bool| ..) |
O switch foi pressionado: um toque, um clique, Space ou Return enquanto ele tem o foco do teclado, ou a ativação por um leitor de tela. Informa o valor que o switch pede como checked, o oposto do checked que ele tem. |
Um switch desabilitado não informa nada.
#Partes
| Parte | Dentro de | Estilizado com | Estados |
|---|---|---|---|
| O switch | class, style |
checked:, disabled:, active:, hover:, focus: (ou checkedStyle, disabledStyle, …) |
|
| O track | o switch, primeiro | trackClass, trackStyle |
checked:, disabled: acompanham o switch |
| O thumb | o track | thumbClass, thumbStyle |
checked:, disabled: acompanham o switch |
| O rótulo | o switch, depois do track | seus próprios elementos (children) |
group-checked: e as outras variantes group-*: com group no class do switch |
O thumb se move pelos seus próprios estilos checked:: uma translação (checked:translate-x-5), ou uma margem ou o alinhamento do track (checked:justify-end no track). Uma classe transition-* anima a mudança de forma nativa. Para colocar o rótulo antes do track, inverta a linha: class="flex-row-reverse justify-between".
#Desabilitado
Um switch desabilitado não recebe pressionamentos, e os leitores de tela o anunciam como esmaecido. Suas classes disabled: e as do track e do thumb se aplicam, sobre as checked::
<Switch
checked={on()}
disabled={!online()}
onChange={({ checked }) => on.set(checked)}
class="flex-row items-center gap-3 disabled:opacity-50"
trackClass="w-11 rounded-full bg-muted p-0.5 checked:bg-primary disabled:bg-muted"
thumbClass="size-5 rounded-full bg-background checked:translate-x-5"
>
<Text>Sync over cellular</Text>
</Switch>
#Um alvo pequeno com hitSlop
Um switch sem rótulo ao lado pode ser menor que um dedo. hitSlop faz com que os pressionamentos que caem perto dele contem, em pontos além de cada lado, sem mudar 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.
<Switch
checked={on()}
hitSlop={{ top: 8, bottom: 8 }}
accessibilityLabel="Notifications"
onChange={({ checked }) => on.set(checked)}
trackClass="w-9 rounded-full bg-muted p-0.5 checked:bg-primary"
thumbClass="size-4 rounded-full bg-background checked:translate-x-4"
/>
#Acessibilidade
- Um switch é um único elemento para leitores de tela, com o papel
"switch"e seu valor (ligado ou desligado); o track e o thumb são partes dele. Seu rótulo é o texto de seus filhos; informeaccessibilityLabelquando não houver. disabledé anunciado como a plataforma o anuncia (VoiceOver, TalkBack, UI Automation, a árvore de acessibilidade do navegador).- No macOS e na web, um switch é 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 switch, 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 switch que tem o foco do teclado.
- Todas as plataformas desenham o que o app estiliza; o thumb se move conforme os estilos e as transições do app indicam.