Checkbox
Um seletor para valores booleanos, usado individualmente para controles de opção ou em grupos para seleção múltipla. Construído com inputs de checkbox nativos e estilização Tailwind para acessibilidade e consistência.
Busque em todas as páginas da documentação
Um seletor para valores booleanos, usado individualmente para controles de opção ou em grupos para seleção múltipla. Construído com inputs de checkbox nativos e estilização Tailwind para acessibilidade e consistência.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
interface CheckboxProps {
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Checkbox({ label, checked, onChange }: CheckboxProps) {
return (
<label className="inline-flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm text-gray-700">{label}</span>
</label>
);
}Um checkbox mínimo com um rótulo de texto. O callback onChange retorna um booleano diretamente para que o componente pai não precise desempacotar e.target.checked. Envolver ambos os elementos dentro de um <label> torna a linha inteira clicável.
"use client";
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
name?: string;
}
export function Checkbox({ checked, onChange, name }: CheckboxProps) {
return (
<input
type="checkbox"
name={name}
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
);
}Um checkbox autônomo sem rótulo, útil quando o rótulo é renderizado separadamente (como em linhas de tabela ou layouts personalizados). A prop name permite que ele participe de envios de formulário nativos.
"use client";
import { useId } from "react";
interface CheckboxProps {
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}
export function Checkbox({ label, checked, onChange, disabled = false }: CheckboxProps) {
const id = useId();
return (
<div className="flex items-center gap-2">
<input
id={id}
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50"
/>
<label
htmlFor={id}
className={`text-sm ${disabled ? "cursor-not-allowed text-gray-400" : "cursor-pointer text-gray-700"}`}
>
{label}
</label>
</div>
);
}Usa useId para gerar um ID estável que vincula o <label> e o <input> via htmlFor. Essa abordagem mantém o rótulo e o input como irmãos, dando mais flexibilidade para o layout do que envolver o input dentro do rótulo.
"use client";
interface Option {
value: string;
label: string;
}
interface CheckboxGroupProps {
label: string;
options: Option[];
selected: string[];
onChange: (selected: string[]) => void;
}
export function CheckboxGroup({ label, options, selected, onChange }: CheckboxGroupProps) {
function toggleValue(value: string) {
onChange(
selected.includes(value)
? selected.filter((v) => v !== value)
: [...selected, value]
);
}
return (
<fieldset>
<legend className="text-sm font-medium text-gray-700">{label}</legend>
<div className="mt-2 space-y-2">
{options.map((opt) => (
<label key={opt.value} className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={selected.includes(opt.value)}
onChange={() => toggleValue(opt.value)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm text-gray-700">{opt.label}</span>
</label>
))}
</div>
</fieldset>
);
}Um grupo de checkboxes gerenciado como um array de valores selecionados. Os elementos <fieldset> e <legend> fornecem agrupamento semântico para leitores de tela. Alternar um valor o adiciona ou o remove do array selecionado.
"use client";
import { useRef, useEffect } from "react";
interface IndeterminateCheckboxProps {
label: string;
checked: boolean;
indeterminate: boolean;
onChange: (checked: boolean) => void;
}
export function IndeterminateCheckbox({
label,
checked,
indeterminate,
onChange,
}: IndeterminateCheckboxProps) {
const checkboxRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
return (
<label className="inline-flex cursor-pointer items-center gap-2">
<input
ref={checkboxRef}
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm font-medium text-gray-700">{label}</span>
</label>
);
}O estado indeterminado é um terceiro estado visual (um traço em vez de um visto) que só pode ser definido via JavaScript, não por atributos HTML. Isso é tipicamente usado para um checkbox "selecionar tudo" quando apenas alguns itens filhos estão marcados. O useEffect define indeterminate diretamente no elemento DOM.
"use client";
import { useId } from "react";
interface CheckboxProps {
label: string;
description: string;
checked: boolean;
onChange: (checked: boolean) => void;
}
export function Checkbox({ label, description, checked, onChange }: CheckboxProps) {
const id = useId();
const descriptionId = `${id}-description`;
return (
<div className="flex items-start gap-3">
<input
id={id}
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
aria-describedby={descriptionId}
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<div>
<label htmlFor={id} className="cursor-pointer text-sm font-medium text-gray-700">
{label}
</label>
<p id={descriptionId} className="text-sm text-gray-500">
{description}
</p>
</div>
</div>
);
}Adiciona uma descrição secundária abaixo do rótulo para contexto adicional. O checkbox é alinhado ao topo do bloco de texto com items-start e mt-0.5. O atributo aria-describedby vincula a descrição ao input para leitores de tela.
"use client";
import { useActionState } from "react";
interface FormState {
message: string;
error?: string;
}
async function submitPreferences(
_prev: FormState,
formData: FormData
): Promise<FormState> {
const accepted = formData.get("terms") === "on";
if (!accepted) {
return { message: "", error: "Você deve aceitar os termos para continuar." };
}
return { message: "Preferências salvas com sucesso!" };
}
export function PreferencesForm() {
const [state, formAction, isPending] = useActionState(submitPreferences, {
message: "",
});
return (
<form action={formAction} className="space-y-4">
<fieldset className="space-y-3">
<legend className="text-sm font-medium text-gray-700">Notificações</legend>
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
name="email_updates"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm text-gray-700">Atualizações por e-mail</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
name="marketing"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm text-gray-700">E-mails de marketing</span>
</label>
</fieldset>
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
name="terms"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
/>
<span className="text-sm text-gray-700">Eu aceito os termos e condições</span>
</label>
{state.error && <p className="text-sm text-red-600">{state.error}</p>}
<button
type="submit"
disabled={isPending}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Salvando..." : "Salvar Preferências"}
</button>
{state.message && <p className="text-sm text-green-600">{state.message}</p>}
</form>
);
}Usa useActionState do React 19 para lidar com o envio de formulário. Checkboxes são não controlados com atributos name para que o navegador os colete no FormData. Um checkbox marcado envia "on" como seu valor; checkboxes não marcados estão completamente ausentes dos dados do formulário.
"use client";
import { forwardRef, useId, useRef, useEffect, useCallback } from "react";
type CheckboxSize = "sm" | "md" | "lg";
interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size" | "onChange"> {
label?: string;
description?: string;
error?: string;
size?: CheckboxSize;
indeterminate?: boolean;
onCheckedChange?: (checked: boolean) => void;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const sizeClasses: Record<CheckboxSize, { box: string; label: string; desc: string }> = {
sm: { box: "h-3.5 w-3.5", label: "text-xs", desc: "text-xs" },
md: { box: "h-4 w-4", label: "text-sm", desc: "text-sm" },
lg: { box: "h-5 w-5", label: "text-base", desc: "text-sm" },
};
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
{
label,
description,
error,
size = "md",
indeterminate = false,
onCheckedChange,
onChange,
disabled,
className,
id: externalId,
...rest
},
ref
) {
const generatedId = useId();
const checkboxId = externalId ?? generatedId;
const errorId = `${checkboxId}-error`;
const descriptionId = `${checkboxId}-desc`;
const internalRef = useRef<HTMLInputElement>(null);
const checkboxRef = (ref as React.RefObject<HTMLInputElement>) ?? internalRef;
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = indeterminate;
}
}, [indeterminate, checkboxRef]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
onCheckedChange?.(e.target.checked);
},
[onChange, onCheckedChange]
);
const hasError = !!error;
const sizes = sizeClasses[size];
const describedBy = [
description ? descriptionId : null,
hasError ? errorId : null,
]
.filter(Boolean)
.join(" ") || undefined;
return (
<div className="flex items-start gap-3">
<input
ref={checkboxRef}
id={checkboxId}
type="checkbox"
disabled={disabled}
onChange={handleChange}
aria-invalid={hasError}
aria-describedby={describedBy}
className={[
"mt-0.5 rounded border-gray-300 text-blue-600",
"focus:ring-2 focus:ring-blue-500 focus:ring-offset-1",
"disabled:cursor-not-allowed disabled:opacity-50",
hasError ? "border-red-500" : "",
sizes.box,
className ?? "",
]
.filter(Boolean)
.join(" ")}
{...rest}
/>
{(label || description || hasError) && (
<div className="flex flex-col">
{label && (
<label
htmlFor={checkboxId}
className={[
"font-medium",
disabled ? "cursor-not-allowed text-gray-400" : "cursor-pointer text-gray-700",
sizes.label,
].join(" ")}
>
{label}
</label>
)}
{description && (
<p id={descriptionId} className={`text-gray-500 ${sizes.desc}`}>
{description}
</p>
)}
{hasError && (
<p id={errorId} className={`mt-0.5 text-red-600 ${sizes.desc}`} role="alert">
{error}
</p>
)}
</div>
)}
</div>
);
});Principais aspectos:
indeterminate é aplicada via useEffect no elemento DOM, pois o HTML não possui um atributo indeterminate. Isso permite padrões de "selecionar tudo" em tabelas de dados.onChange quanto um callback booleano simplificado onCheckedChange, tornando-o compatível com bibliotecas de formulário e estado simples.Valor do Checkbox em FormData é "on", não true -- Ao usar o envio de formulário nativo, um checkbox marcado envia "on" como seu valor. Checkboxes não marcados estão completamente ausentes do FormData, não "off". Use formData.has("name") para verificar a existência.
indeterminate não é um atributo HTML -- Você não pode definir o estado indeterminado através de props JSX. Ele deve ser definido através de uma ref com el.indeterminate = true. Esquecer isso leva o estado indeterminado a nunca aparecer.
Checkbox controlado precisa de checked e onChange -- Fornecer checked sem onChange torna o checkbox somente leitura e dispara um aviso do React. Se você quiser um checkbox somente leitura, passe também readOnly.
defaultChecked vs checked -- Usar defaultChecked torna o checkbox não controlado. Alternar entre defaultChecked e checked em tempo de execução causa comportamento imprevisível. Escolha uma abordagem por instância de componente.
Serialização de grupo de checkboxes -- Quando vários checkboxes compartilham o mesmo name, FormData.getAll("name") retorna um array de strings "on". Dê a cada checkbox um name exclusivo ou use um atributo de valor: <input type="checkbox" name="colors" value="red" />.
Área de clique muito pequena -- Um checkbox nu é um alvo pequeno. Sempre envolva-o em um <label> ou use preenchimento suficiente ao redor dele para atender aos tamanhos mínimos de alvo de toque (pelo menos 44x44px no celular).
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥