Toggle
ProntoLiga/desliga binário com efeito imediato. Diferente de Checkbox: a mudança aplica na hora, sem botão Salvar.
Usar quando
Configurações que aplicam imediatamente: notificações, modo escuro, recursos opcionais, ativar/desativar canal.
Não usar quando
Mudança que requer confirmação (use Checkbox + Salvar). Estado de form que ainda vai ser submetido. Mais de 2 estados.
Variantes
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Track slide | click | Track passa de border (off) para ink (on) com fade | 200ms cubic-bezier(0.32,0.72,0,1) |
| Thumb slide | click | Thumb desliza horizontalmente da esquerda para direita | 250ms cubic-bezier(0.32,0.72,0,1) |
| Thumb scale | mousedown | Thumb expande horizontalmente (alongado) durante drag | 100ms ease |
| Hover background | mouseenter | Track ganha leve glow (border ou ink ficam +5% saturated) | 150ms ease |
| Loading spinner | loading=true | Spinner gira no thumb enquanto async | 600ms linear infinite |
| Optimistic toggle | click | Aplica mudança visual antes da confirmação do servidor | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o toggle |
| Space / Enter | Alterna on/off |
ARIA esperado
- role="switch" // não checkbox
- aria-checked="true" | "false"
- aria-label="..." // se sem label visual
- aria-disabled="true" se disabled
Notas
- Use role="switch" — semantica diferente de checkbox.
- A mudança aplica IMEDIATAMENTE — não tem botão Salvar.
- Em async, use loading state + optimistic update + rollback se falhar.
- Erros: reverte estado + toast de erro.
- Toggle NUNCA é usado dentro de form que precisa Submit.
Código
'use client';
import { T, MOTION, transition } from '@/lib/tokens';
interface ToggleProps {
on: boolean;
onChange: (on: boolean) => void;
disabled?: boolean;
loading?: boolean;
size?: 'sm' | 'md';
}
export function Toggle({ on, onChange, disabled, loading, size = 'md' }: ToggleProps) {
const w = size === 'sm' ? 32 : 40;
const h = size === 'sm' ? 18 : 22;
const thumb = size === 'sm' ? 14 : 18;
return (
<button
type="button"
role="switch"
aria-checked={on}
aria-disabled={disabled}
onClick={() => !disabled && onChange(!on)}
disabled={disabled}
style={{
width: w, height: h, borderRadius: 980,
background: on ? T.ink : T.border,
border: 'none',
position: 'relative',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: transition('background-color', 'base'),
padding: 0,
}}
>
<span style={{
position: 'absolute',
top: (h - thumb) / 2,
left: on ? w - thumb - (h - thumb) / 2 : (h - thumb) / 2,
width: thumb, height: thumb, borderRadius: 980,
background: T.surface,
boxShadow: T.shadowSm,
transition: transition(['left', 'transform'], 'base'),
}} />
</button>
);
}Regras
Faça
- ✓Use role="switch" (não checkbox).
- ✓Aplica imediatamente — sem botão Salvar.
- ✓Optimistic update: aplica visual antes do servidor confirmar.
- ✓Em erro async: reverte + toast.
- ✓Track preto (T.ink) quando ON, cinza (T.border) quando OFF.
- ✓Thumb branco com sombra sutil.
Não faça
- ✗Não use toggle dentro de form com Submit.
- ✗Não use cor (verde, vermelho) para indicar state — preto/cinza basta.
- ✗Não exiba labels ON/OFF dentro do track (ruidoso).
- ✗Não desabilite só visualmente — use disabled real.
- ✗Não force confirmação para toggle (esse é o ponto: imediato).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.ink | #0A0A0A | track quando ON |
T.border | #E3DFD2 | track quando OFF |
T.surface | #FFFFFF | thumb |
T.shadowSm | 0 1px 2px rgba(...) | sombra do thumb |
RADIUS.pill | 980 | forma |
MOTION.base | 250ms | thumb slide |