Switch
Um controle de alternância liga/desliga que representa visualmente um estado booleano, comumente usado para configurações, preferências e flags de recursos.
Busque em todas as páginas da documentação
Um controle de alternância liga/desliga que representa visualmente um estado booleano, comumente usado para configurações, preferências e flags de recursos.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
import { useState } from "react";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Switch({ checked, onChange }: SwitchProps) {
return (
<button
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
checked ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
}Um alternador mínimo construído sobre um <button> com role="switch" e aria-checked para acessibilidade. O polegar desliza entre as posições usando translate-x e a cor da trilha transiciona entre cinza e azul. Nenhum checkbox oculto é necessário porque o próprio botão atua como o controle do formulário.
"use client";
import { useId } from "react";
interface SwitchProps {
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Switch({ label, checked, onChange }: SwitchProps) {
const id = useId();
return (
<div className="flex items-center gap-3">
<button
id={id}
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
checked ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<label htmlFor={id} className="text-sm font-medium text-gray-700 cursor-pointer">
{label}
</label>
</div>
);
}O rótulo está conectado ao switch via htmlFor e o id gerado, então clicar no texto do rótulo também alterna o switch. A classe cursor-pointer sinaliza que o rótulo é interativo.
"use client";
import { useId } from "react";
interface SwitchProps {
label: string;
description: string;
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Switch({ label, description, checked, onChange }: SwitchProps) {
const id = useId();
const descId = `${id}-desc`;
return (
<div className="flex items-start justify-between gap-4">
<div>
<label htmlFor={id} className="text-sm font-medium text-gray-900 cursor-pointer">
{label}
</label>
<p id={descId} className="text-sm text-gray-500">
{description}
</p>
</div>
<button
id={id}
role="switch"
aria-checked={checked}
aria-describedby={descId}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${
checked ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
);
}Um layout estilo configurações com o rótulo e a descrição à esquerda, switch à direita. O aria-describedby vincula a descrição ao switch para que os leitores de tela anunciem o contexto suplementar. A classe shrink-0 impede que o switch seja espremido por texto longo.
"use client";
type SwitchSize = "sm" | "md" | "lg";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
size?: SwitchSize;
}
const sizeClasses: Record<SwitchSize, { track: string; thumb: string; translate: string }> = {
sm: { track: "h-5 w-9", thumb: "h-3 w-3", translate: "translate-x-5" },
md: { track: "h-6 w-11", thumb: "h-4 w-4", translate: "translate-x-6" },
lg: { track: "h-8 w-14", thumb: "h-6 w-6", translate: "translate-x-7" },
};
export function Switch({ checked, onChange, size = "md" }: SwitchProps) {
const s = sizeClasses[size];
return (
<button
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex items-center rounded-full transition-colors ${s.track} ${
checked ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block rounded-full bg-white transition-transform ${s.thumb} ${
checked ? s.translate : "translate-x-1"
}`}
/>
</button>
);
}Um mapa de tamanho mantém a trilha e o polegar proporcionais em cada breakpoint. A distância de translação se ajusta por tamanho para que o polegar fique alinhado com a borda da trilha em ambos os estados.
"use client";
type SwitchColor = "blue" | "green" | "red";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
color?: SwitchColor;
}
const colorClasses: Record<SwitchColor, string> = {
blue: "bg-blue-600",
green: "bg-green-600",
red: "bg-red-600",
};
export function Switch({ checked, onChange, color = "blue" }: SwitchProps) {
return (
<button
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
checked ? colorClasses[color] : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
}Cores diferentes comunicam intenção -- verde para sucesso/habilitar, vermelho para destrutivo/perigo, azul para configurações neutras. O estado desativado permanece cinza em todas as variantes para consistência.
"use client";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
label?: string;
}
export function Switch({ checked, onChange, disabled = false, label }: SwitchProps) {
return (
<div className="flex items-center gap-3">
<button
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
disabled
? "cursor-not-allowed opacity-50"
: ""
} ${checked ? "bg-blue-600" : "bg-gray-300"}`}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
{label && (
<span className={`text-sm font-medium ${disabled ? "text-gray-400" : "text-gray-700"}`}>
{label}
</span>
)}
</div>
);
}O atributo disabled no botão impede cliques nativamente. As classes opacity-50 e cursor-not-allowed dão um sinal visual claro de que o controle está inativo. O texto do rótulo também escurece para reforçar o estado desabilitado.
"use client";
import { forwardRef, useId, useCallback } from "react";
type SwitchSize = "sm" | "md" | "lg";
type SwitchColor = "blue" | "green" | "red";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
label?: string;
description?: string;
size?: SwitchSize;
color?: SwitchColor;
disabled?: boolean;
name?: string;
id?: string;
className?: string;
}
const sizeClasses: Record<SwitchSize, { track: string; thumb: string; translate: string }> = {
sm: { track: "h-5 w-9", thumb: "h-3 w-3", translate: "translate-x-5" },
md: { track: "h-6 w-11", thumb: "h-4 w-4", translate: "translate-x-6" },
lg: { track: "h-8 w-14", thumb: "h-6 w-6", translate: "translate-x-7" },
};
const colorClasses: Record<SwitchColor, string> = {
blue: "bg-blue-600",
green: "bg-green-600",
red: "bg-red-600",
};
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch(
{
checked,
onChange,
label,
description,
size = "md",
color = "blue",
disabled = false,
name,
id: externalId,
className,
},
ref
) {
const generatedId = useId();
const switchId = externalId ?? generatedId;
const descId = `${switchId}-desc`;
const s = sizeClasses[size];
const handleClick = useCallback(() => {
if (!disabled) onChange(!checked);
}, [disabled, checked, onChange]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (disabled) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onChange(!checked);
}
},
[disabled, checked, onChange]
);
return (
<div className={`flex items-start justify-between gap-4 ${className ?? ""}`}>
{(label || description) && (
<div className="min-w-0">
{label && (
<label
htmlFor={switchId}
className={`block text-sm font-medium cursor-pointer ${
disabled ? "text-gray-400" : "text-gray-900"
}`}
>
{label}
</label>
)}
{description && (
<p
id={descId}
className={`text-sm ${disabled ? "text-gray-300" : "text-gray-500"}`}
>
{description}
</p>
)}
</div>
)}
{/* Input oculto para serialização de formulário */}
{name && (
<input type="hidden" name={name} value={checked ? "on" : "off"} />
)}
<button
ref={ref}
id={switchId}
role="switch"
type="button"
aria-checked={checked}
aria-describedby={description ? descId : undefined}
disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
className={[
"relative inline-flex shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
s.track,
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
checked ? colorClasses[color] : "bg-gray-300",
].join(" ")}
>
<span
aria-hidden="true"
className={[
"inline-block rounded-full bg-white shadow-sm transition-transform duration-200",
s.thumb,
checked ? s.translate : "translate-x-1",
].join(" ")}
/>
</button>
</div>
);
});Aspectos Chave:
name é fornecida, um input oculto é renderizado para que o valor do switch seja incluído em submissões de formulário nativas e FormData.focus-visible aparece apenas para navegação por teclado, não para cliques do mouse, proporcionando um visual limpo e mantendo a acessibilidade.Enter e Espaço garantem um comportamento consistente entre os navegadores, pois alguns navegadores não disparam click no Espaço para controles não nativos.duration-200 tanto na trilha quanto no polegar cria uma animação suave e coordenada sem parecer lenta.Falta de role="switch" -- sem essa função, os leitores de tela tratam o elemento como um botão simples. O atributo aria-checked só é válido quando a função é switch ou checkbox.
Usar um checkbox em vez de um botão -- um checkbox oculto com uma sobreposição visual funciona, mas requer manipulação cuidadosa do teclado e associação de rótulo. Um <button> com role="switch" é mais simples e previsível.
Transição não animando -- se você alternar classes que o Tailwind remove (por exemplo, translate-x-6), a classe não existirá em produção. Certifique-se de que todos os valores de translação apareçam na safelist da sua configuração do Tailwind ou sejam sempre referenciados na origem.
Posição do polegar com um pixel de diferença -- a distância de translação do polegar deve levar em conta o preenchimento da trilha. Se o polegar não ficar alinhado com a borda da trilha, ajuste o valor de translação ou adicione preenchimento à trilha.
Não usar type="button" -- dentro de um formulário, um <button> tem o padrão type="submit", que enviará o formulário quando o switch for clicado. Sempre adicione type="button" para evitar isso.
Manipulador de clique dispara em desabilitado -- embora o atributo nativo disabled impeça eventos de clique, alguns padrões de delegação de eventos ou manipuladores onClick de wrapper podem ainda disparar. Sempre proteja com um retorno antecipado no manipulador.
Sem serialização de valor de formulário -- ao contrário de um checkbox nativo, um switch baseado em botão não aparece automaticamente no FormData. Inclua um input oculto com o estado do switch para suportar submissões de formulário nativas.
Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥