Etiqueta
Una etiqueta de texto accesible que se empareja con controles de formulario mediante htmlFor, proporcionando identificación clara y comportamiento de clic para enfocar inputs, selects y otros elementos interactivos.
Casos de uso
- Etiquetar text inputs, selects y textareas en formularios
- Indicar campos requeridos con un asterisco visual
- Marcar campos opcionales para reducir confusión del usuario
- Proporcionar ayuda en línea mediante un icono de tooltip junto a la etiqueta
- Envolver la etiqueta e input juntos como un componente de campo de formulario reutilizable
- Mostrar estilos de error de validación junto al texto de la etiqueta
- Asociar etiquetas con controles personalizados como switches o sliders
Implementación más simple
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>
);
}Un componente de etiqueta minimal que renderiza un elemento <label> con estilo. La prop htmlFor la conecta con un control de formulario por id, por lo que hacer clic en la etiqueta enfoca el input asociado. Este componente no necesita "use client" porque no tiene estado ni manejadores de eventos.
Variaciones
Con indicador de requerido
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>
);
}El asterisco rojo señala un campo requerido visualmente. El aria-hidden="true" evita que los lectores de pantalla anuncien "asterisco" -- el input en sí debe usar aria-required="true" o el atributo required para comunicar el requisito de forma programática.
Con indicador de opcional
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>
);
}Cuando la mayoría de campos en un formulario son requeridos, marcar los pocos opcionales es menos ruidoso que agregar asteriscos a cada campo requerido. El texto más pequeño y ligero distingue claramente el indicador del contenido de la etiqueta.
Con icono de ayuda en tooltip
"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>
);
}Un icono de ayuda junto a la etiqueta revela contexto adicional al pasar el mouse. El tooltip se posiciona arriba del icono con bottom-full y se centra con -translate-x-1/2. El span del tooltip está anidado dentro de la etiqueta pero usa onMouseEnter/onMouseLeave en su propio wrapper para delimitar el área de hover.
Como envoltura de campo de formulario
"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>El patrón render-prop pasa el id generado al input hijo, asegurando que la etiqueta e input siempre estén conectados sin que el consumidor maneje los IDs manualmente. El mensaje de error usa role="alert" para que los lectores de pantalla lo anuncien inmediatamente.
Con estilos de error
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>
);
}Cuando un campo tiene un error de validación, la etiqueta se vuelve roja para llamar la atención al área del problema. Esto se empareja con un borde rojo en el input y un mensaje de error debajo. El color del asterisco también cambia para coincidir con el estado de error para consistencia visual.
Etiqueta oculta visualmente
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>
);
}La clase sr-only oculta la etiqueta visualmente mientras la mantiene en el árbol de accesibilidad. Esto es útil para inputs de búsqueda o botones de icono donde una etiqueta visible haría desordenado el diseño pero los lectores de pantalla aún necesitan texto descriptivo.
Implementación compleja
"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 clave:
- Prop
aspolimórfica -- renderiza como<label>o<span>dependiendo del contexto. Cuando se usa dentro de un wrapper<label>, renderizar como<span>evita etiquetas anidadas que es HTML inválido. - forwardRef -- permite que los componentes padre accedan al elemento DOM subyacente para gestión de enfoque o medición.
- Indicadores mutuamente excluyentes --
requiredyoptionalestán protegidos para que solo uno se renderice a la vez, evitando indicadores conflictivos confusos. - Tooltip con soporte de teclado -- el icono de ayuda es un
<button>enfocable con manejadoresonFocus/onBlur, así los usuarios de teclado pueden acceder al tooltip tabulando hacia él. Elaria-describedbyvincula el botón al contenido del tooltip. - Estado deshabilitado -- la etiqueta se oscurece a
text-gray-400cuando el control asociado está deshabilitado, reforzando el estado inactivo visualmente. - Ruta rápida sr-only -- cuando
srOnlyes true, el componente retorna temprano con solo la clase screen-reader-only, evitando elementos DOM innecesarios para el tooltip o indicadores. - Propagación del color de error -- tanto el texto de la etiqueta como el asterisco requerido cambian a rojo en el estado de error, creando una conexión visual clara con el borde de error del input.
Trampas
-
Emparejamiento
htmlForeidfaltante -- si elhtmlForde la etiqueta no coincide con elidde ningún elemento, hacer clic en la etiqueta no hace nada. Siempre asegúrate de que los IDs coincidan, o envuelve el input dentro del elemento label. -
Elementos
<label>anidados -- envolver un componente de etiqueta dentro de otro<label>es HTML inválido y causa comportamiento impredecible. Usa el patrónas="span"si compones etiquetas dentro de un wrapper de etiqueta más grande. -
Asterisco anunciado por lectores de pantalla -- si el asterisco
*carece dearia-hidden="true", los lectores de pantalla anunciarán "asterisco" después del texto de la etiqueta. Siempre oculta indicadores decorativos y confía enaria-requireden el input en su lugar. -
IDs dinámicos causando desincronización de hidratación -- usar
Math.random()oDate.now()para IDs genera valores diferentes en servidor y cliente, causando errores de hidratación de React. UsauseId()para IDs únicos seguros para SSR. -
Etiqueta no se actualiza con estado de error -- si el color de la etiqueta no cambia cuando aparece un error de validación, el usuario puede no notar cuál campo tiene el problema. Siempre pasa el estado de error a la etiqueta, no solo al input.
-
Tooltip dentro de la etiqueta intercepta clics -- elementos interactivos dentro de un
<label>pueden causar que el clic de la etiqueta dispare en el botón del tooltip en lugar de enfocar el input asociado. Usae.preventDefault()en el botón del tooltip o coloca el tooltip fuera de la etiqueta.
Relacionado
- Input -- El control más común emparejado con etiquetas
- Switch -- Controles de alternancia que se benefician de la asociación de etiqueta
- Formularios -- Patrones de formulario y estrategias de validación
- Tooltip -- Componente de tooltip independiente para contenido hover más complejo