Label
Um rótulo de texto acessível que se associa a controles de formulário via htmlFor, fornecendo identificação clara e comportamento de clique para foco em entradas, seleções e outros elementos interativos.
Busque em todas as páginas da documentação
Um rótulo de texto acessível que se associa a controles de formulário via htmlFor, fornecendo identificação clara e comportamento de clique para foco em entradas, seleções e outros elementos interativos.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
}
export function Label({ htmlFor, children }: LabelProps) {
return (
<label
htmlFor={htmlFor}
className="block text-sm font-medium text-gray-700"
>
{children}
</label>
);
}Um componente de rótulo mínimo que renderiza um elemento <label> estilizado. A prop htmlFor o conecta a um controle de formulário por id, então clicar no rótulo foca a entrada associada. Este componente não precisa de "use client" pois não possui estado ou manipuladores de eventos.
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
required?: boolean;
}
export function Label({ htmlFor, children, required = false }: LabelProps) {
return (
<label htmlFor={htmlFor} className="block text-sm font-medium text-gray-700">
{children}
{required && <span className="ml-1 text-red-500" aria-hidden="true">*</span>}
</label>
);
}O asterisco vermelho sinaliza visualmente um campo obrigatório. O aria-hidden="true" impede que leitores de tela anunciem "asterisco" -- a própria entrada deve usar aria-required="true" ou o atributo required para comunicar o requisito programaticamente.
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
optional?: boolean;
}
export function Label({ htmlFor, children, optional = false }: LabelProps) {
return (
<label htmlFor={htmlFor} className="block text-sm font-medium text-gray-700">
{children}
{optional && (
<span className="ml-1.5 text-xs font-normal text-gray-400">(optional)</span>
)}
</label>
);
}Quando a maioria dos campos em um formulário é obrigatória, marcar os poucos opcionais é menos barulhento do que adicionar asteriscos a cada campo obrigatório. O texto menor e mais claro distingue claramente o indicador do conteúdo do rótulo.
"use client";
import { useState } from "react";
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
tooltip: string;
}
export function Label({ htmlFor, children, tooltip }: LabelProps) {
const [showTooltip, setShowTooltip] = useState(false);
return (
<label htmlFor={htmlFor} className="inline-flex items-center gap-1.5 text-sm font-medium text-gray-700">
{children}
<span
className="relative"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<svg
className="h-4 w-4 cursor-help text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M12 2a10 10 0 100 20 10 10 0 000-20z"
/>
</svg>
{showTooltip && (
<span className="absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs font-normal text-white">
{tooltip}
</span>
)}
</span>
</label>
);
}Um ícone de ajuda ao lado do rótulo revela contexto adicional ao passar o mouse. O tooltip é posicionado acima do ícone com bottom-full e centralizado com -translate-x-1/2. O span do tooltip está aninhado dentro do rótulo, mas usa onMouseEnter/onMouseLeave em seu próprio wrapper para delimitar a área de passagem do mouse.
"use client";
import { useId } from "react";
interface FormFieldProps {
label: string;
error?: string;
required?: boolean;
children: (id: string) => React.ReactNode;
}
export function FormField({ label, error, required, children }: FormFieldProps) {
const id = useId();
return (
<div className="space-y-1">
<label htmlFor={id} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="ml-1 text-red-500" aria-hidden="true">*</span>}
</label>
{children(id)}
{error && (
<p className="text-sm text-red-600" role="alert">
{error}
</p>
)}
</div>
);
}
// Uso:
// <FormField label="Email" required error={errors.email}>
// {(id) => (
// <input
// id={id}
// type="email"
// className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
// />
// )}
// </FormField>O padrão render-prop passa o id gerado para a entrada filha, garantindo que o rótulo e a entrada estejam sempre conectados sem que o consumidor precise gerenciar IDs manualmente. A mensagem de erro usa role="alert" para que leitores de tela a anunciem imediatamente.
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
error?: boolean;
required?: boolean;
}
export function Label({ htmlFor, children, error = false, required = false }: LabelProps) {
return (
<label
htmlFor={htmlFor}
className={`block text-sm font-medium ${
error ? "text-red-600" : "text-gray-700"
}`}
>
{children}
{required && (
<span className={`ml-1 ${error ? "text-red-600" : "text-red-500"}`} aria-hidden="true">
*
</span>
)}
</label>
);
}Quando um campo tem um erro de validação, o rótulo fica vermelho para chamar a atenção para a área do problema. Isso combina com uma borda vermelha na entrada e uma mensagem de erro abaixo dela. A cor do asterisco também muda para corresponder ao estado de erro para consistência visual.
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
srOnly?: boolean;
}
export function Label({ htmlFor, children, srOnly = false }: LabelProps) {
return (
<label
htmlFor={htmlFor}
className={
srOnly
? "sr-only"
: "block text-sm font-medium text-gray-700"
}
>
{children}
</label>
);
}A classe sr-only oculta o rótulo visualmente, mantendo-o na árvore de acessibilidade. Isso é útil para entradas de pesquisa ou botões de ícone onde um rótulo visível poluiria o design, mas leitores de tela ainda precisam de texto descritivo.
"use client";
import { forwardRef, useId, useState } from "react";
interface LabelProps {
htmlFor?: string;
children: React.ReactNode;
required?: boolean;
optional?: boolean;
error?: boolean;
disabled?: boolean;
tooltip?: string;
srOnly?: boolean;
className?: string;
as?: "label" | "span";
}
export const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label(
{
htmlFor,
children,
required = false,
optional = false,
error = false,
disabled = false,
tooltip,
srOnly = false,
className,
as: Component = "label",
},
ref
) {
const [showTooltip, setShowTooltip] = useState(false);
const tooltipId = useId();
if (srOnly) {
return (
<Component
ref={ref as React.Ref<HTMLLabelElement>}
htmlFor={Component === "label" ? htmlFor : undefined}
className="sr-only"
>
{children}
</Component>
);
}
const textColor = (() => {
if (disabled) return "text-gray-400";
if (error) return "text-red-600";
return "text-gray-700";
})();
return (
<Component
ref={ref as React.Ref<HTMLLabelElement>}
htmlFor={Component === "label" ? htmlFor : undefined}
className={`inline-flex items-center gap-1.5 text-sm font-medium ${textColor} ${className ?? ""}`}
>
<span>{children}</span>
{required && !optional && (
<span className={error ? "text-red-600" : "text-red-500"} aria-hidden="true">
*
</span>
)}
{optional && !required && (
<span className="text-xs font-normal text-gray-400">(optional)</span>
)}
{tooltip && (
<span
className="relative"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onFocus={() => setShowTooltip(true)}
onBlur={() => setShowTooltip(false)}
>
<button
type="button"
tabIndex={0}
aria-describedby={showTooltip ? tooltipId : undefined}
className="inline-flex items-center text-gray-400 hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:rounded"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M12 2a10 10 0 100 20 10 10 0 000-20z"
/>
</svg>
</button>
{showTooltip && (
<span
id={tooltipId}
role="tooltip"
className="absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs font-normal text-white shadow-lg"
>
{tooltip}
</span>
)}
</span>
)}
</Component>
);
});Aspectos chave:
as polimórfica -- renderiza como <label> ou <span> dependendo do contexto. Quando usado dentro de um wrapper <label>, renderizar como <span> evita rótulos aninhados, que são HTML inválido.forwardRef -- permite que componentes pais acessem o elemento DOM subjacente para gerenciamento de foco ou medição.required e optional são protegidos para que apenas um possa renderizar por vez, evitando indicadores conflitantes confusos.<button> focável com manipuladores onFocus/onBlur, para que usuários de teclado possam acessar o tooltip ao tabulá-lo. O aria-describedby vincula o botão ao conteúdo do tooltip.text-gray-400 quando o controle associado está desabilitado, reforçando visualmente o estado inativo.sr-only -- quando srOnly é verdadeiro, o componente retorna cedo apenas com a classe sr-only, evitando elementos DOM desnecessários para o tooltip ou indicadores.Pareamento ausente de htmlFor e id -- se o htmlFor do rótulo não corresponder ao id de nenhum elemento, clicar no rótulo não faz nada. Sempre garanta que os IDs correspondam, ou envolva a entrada dentro do elemento de rótulo.
Elementos <label> aninhados -- envolver um componente de rótulo dentro de outro <label> é HTML inválido e causa comportamento imprevisível. Use o padrão as="span" se estiver compondo rótulos dentro de um wrapper de rótulo maior.
Asterisco anunciado por leitores de tela -- se o asterisco * não tiver aria-hidden="true", leitores de tela anunciarão "asterisco" após o texto do rótulo. Sempre oculte indicadores decorativos e confie em aria-required na entrada em vez disso.
IDs dinâmicos causando dessincronização de hidratação -- usar Math.random() ou Date.now() para IDs gera valores diferentes no servidor e no cliente, causando erros de hidratação do React. Use useId() para IDs únicos seguros para SSR.
Rótulo não atualizando com estado de erro -- se a cor do rótulo não mudar quando um erro de validação aparece, o usuário pode não notar qual campo tem o problema. Sempre passe o estado de erro para o rótulo, não apenas para a entrada.
Tooltip dentro do rótulo intercepta cliques -- elementos interativos dentro de um <label> podem fazer com que o clique do rótulo dispare no botão do tooltip em vez de focar a entrada associada. Use e.preventDefault() no botão do tooltip ou coloque o tooltip fora do rótulo.
Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥