Field + form con validación
Todo formulario. Validá al salir del campo y al enviar; el error dice cómo arreglarlo.
Composición de label, control, ayuda y error. Field organiza el espacio: la validación y el envío quedan a cargo del formulario.
Instalación
shadcn
npx shadcn@latest add https://design.moodinary.com/r/field.jsonimport
import { Field, FieldLabel, FieldHint, FieldError, FieldLegend } from "@moodinary/ui/components/field";Ejemplos
Meta semanal
Replica WeeklyTargetForm: ayuda y error asociados al control, validación al enviar y foco en el campo inválido. La confirmación del ejemplo es local, no guarda una meta real.
Código
default.tsx
"use client";
import { Button } from "@moodinary/ui/components/button";
import {
Field,
FieldError,
FieldHint,
FieldLabel,
} from "@moodinary/ui/components/field";
import { Input } from "@moodinary/ui/components/input";
import { useCallback, useState } from "react";
const DIGITS = /^\d+$/;
export default function FieldDefault() {
const [value, setValue] = useState("");
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
const change = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
setError(null);
setSaved(false);
}, []);
const submit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (
value &&
(!DIGITS.test(value) || Number(value) < 1 || Number(value) > 1_000_000)
) {
setError("Ingresá un entero entre 1 y 1.000.000.");
(
event.currentTarget.elements.namedItem("target") as HTMLInputElement
).focus();
return;
}
setSaved(true);
},
[value]
);
return (
<form className="w-full max-w-[520px]" noValidate onSubmit={submit}>
<section className="space-y-5 rounded-[10px] border border-line bg-card p-4 text-ink text-sm shadow-card">
<div>
<h3 className="font-medium text-foreground text-sm">Meta semanal</h3>
<p className="mt-0.5 text-muted-foreground text-xs">
Cuántos pulsos esperás por semana. Inicio la usa para mostrar el
avance; dejala vacía para no tener meta.
</p>
</div>
<Field>
<FieldLabel htmlFor="weekly-target-field">
Pulsos por semana
</FieldLabel>
<Input
aria-describedby={
error ? "target-hint target-error" : "target-hint"
}
aria-invalid={error ? true : undefined}
autoComplete="off"
id="weekly-target-field"
inputMode="numeric"
name="target"
onChange={change}
placeholder="Sin meta"
value={value}
/>
<FieldHint id="target-hint">Entero entre 1 y 1.000.000.</FieldHint>
<FieldError id="target-error">{error}</FieldError>
</Field>
{saved ? (
<p aria-live="polite" className="text-m5 text-xs">
Validación local correcta. No se guardaron cambios.
</p>
) : null}
<div className="flex flex-wrap justify-end gap-2">
<Button type="submit">Guardar meta</Button>
</div>
</section>
</form>
);
}Props
Field
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| ...props | React.ComponentProps<"div"> | - | Contenedor vertical para label, input y mensajes. |
FieldLabel
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| htmlFor | string | - | ID del control asociado. |
| required | boolean | false | Agrega el asterisco visual. El control también necesita required. |
| children | React.ReactNode | - | Nombre visible del campo. |
FieldHint
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| id | string | - | Referencia para aria-describedby del control. |
| children | React.ReactNode | - | Ayuda breve, con color de texto secundario. |
FieldError
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| children | React.ReactNode | - | Error concreto; sin contenido no renderiza nada. |
| id | string | - | Referencia para aria-describedby del control. |
| role | React.AriaRole | "alert" | Anuncia el mensaje cuando aparece. |
FieldLegend
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| required | boolean | false | Agrega el asterisco a la pregunta de un fieldset. |
| ...props | React.ComponentProps<"legend"> | - | Legend nativo para agrupar opciones relacionadas. |
Cuándo sí y cuándo no
Hacé
- Validá al enviar y limpiá el error cuando se corrige el campo.
- Explicá cómo corregir el dato.
- Enfocá el primer campo inválido al enviar.
Evitá
- No creas que required en FieldLabel valida el input.
- No muestres errores sin asociarlos al control.
- No confundas la confirmación del ejemplo con una acción en el servidor.
Accesibilidad
- Tab recorre los controles en orden; Enter puede enviar el formulario.
- Conectá label y control con htmlFor e id.
- Usá aria-invalid y aria-describedby en el control. FieldError anuncia el mensaje con role alert.