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.
Busca en todas las páginas de la documentación
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.
🤖 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>
);
}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.
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.
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.
"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.
"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.
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.
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.
"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:
as polimó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.required y optional están protegidos para que solo uno se renderice a la vez, evitando indicadores conflictivos confusos.<button> enfocable con manejadores onFocus/onBlur, así los usuarios de teclado pueden acceder al tooltip tabulando hacia él. El aria-describedby vincula el botón al contenido del tooltip.text-gray-400 cuando el control asociado está deshabilitado, reforzando el estado inactivo visualmente.srOnly es true, el componente retorna temprano con solo la clase screen-reader-only, evitando elementos DOM innecesarios para el tooltip o indicadores.Emparejamiento htmlFor e id faltante -- si el htmlFor de la etiqueta no coincide con el id de 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ón as="span" si compones etiquetas dentro de un wrapper de etiqueta más grande.
Asterisco anunciado por lectores de pantalla -- si el asterisco * carece de aria-hidden="true", los lectores de pantalla anunciarán "asterisco" después del texto de la etiqueta. Siempre oculta indicadores decorativos y confía en aria-required en el input en su lugar.
IDs dinámicos causando desincronización de hidratación -- usar Math.random() o Date.now() para IDs genera valores diferentes en servidor y cliente, causando errores de hidratación de React. Usa useId() 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. Usa e.preventDefault() en el botón del tooltip o coloca el tooltip fuera de la etiqueta.
Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥