Field
Un campo: la etiqueta, la ayuda y el error atados al control, sin un solo id escrito a mano.
import { Field, FieldControl, FieldDescription, FieldError, … } from "sebs7n-ui/field"Ejemplos
Un campo completo
La etiqueta, la ayuda y el error atados al control sin un solo `id` escrito a mano. Comparalo con armar lo mismo suelto: `useId`, `htmlFor`, `aria-describedby` condicional y `aria-invalid`.
import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"
function Basico() {
return (
<Field className="w-full max-w-sm" name="cuit">
<FieldLabel required>CUIT</FieldLabel>
<Input placeholder="30712345678" required />
<FieldDescription>Once dígitos, sin guiones.</FieldDescription>
<FieldError />
</Field>
)
}Cuándo se valida
`validationMode="onBlur"` marca el error recién al salir del campo. En `onChange` el rojo aparece a la segunda letra del email, cuando todavía falta todo: sirve para un campo que se puede evaluar entero mientras se escribe, como el largo de una contraseña.
import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"
function Validacion() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<Field
name="email"
validate={(valor) => (String(valor).includes("@") ? null : "Falta el @: revisá el email")}
validationMode="onBlur"
>
<FieldLabel>Email</FieldLabel>
<Input placeholder="vos@empresa.com" type="email" />
<FieldError />
</Field>
<Field
name="password"
validate={(valor) => (String(valor).length >= 8 ? null : "Mínimo 8 caracteres")}
validationMode="onChange"
>
<FieldLabel>Contraseña</FieldLabel>
<Input type="password" />
<FieldDescription>Se valida mientras escribís porque acá sí se puede saber desde el primer carácter.</FieldDescription>
<FieldError />
</Field>
</div>
)
}El error que solo conoce el servidor
Que un email ya esté usado no se puede validar en el navegador. `Form` recibe un objeto `{ campo: mensaje }` y cada `FieldError` muestra el suyo, en el campo que corresponde y no en un cartel arriba de todo.
import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { useState } from "react"
function ErroresDelServidor() {
const [errors, setErrors] = useState<Record<string, string>>({})
const [enviando, setEnviando] = useState(false)
return (
<Form
className="w-full max-w-sm"
errors={errors}
onFormSubmit={async (valores) => {
setEnviando(true)
await new Promise((r) => setTimeout(r, 600))
setEnviando(false)
// El servidor de mentira: cualquier email de este dominio ya está tomado.
setErrors(
String(valores.email).endsWith("@acme.com") ? { email: "Ese email ya tiene una cuenta" } : {}
)
}}
>
<Field name="email">
<FieldLabel required>Email</FieldLabel>
<Input placeholder="probá con algo@acme.com" required type="email" />
<FieldError />
</Field>
<Button loading={enviando} type="submit">
Crear cuenta
</Button>
</Form>
)
}Obligatorio que valida el servidor
`FieldLabel indicator` dibuja el asterisco y el lector dice «Razón social, obligatorio», sin `required` en el control: el formulario no corta el envío y el error llega del servidor por `Form`.
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"
function IndicadorSinValidar() {
return (
<Field className="w-full max-w-sm" name="businessName">
<FieldLabel indicator>Razón social</FieldLabel>
<Input placeholder="Estudio Ruiz S.R.L." />
<FieldError />
</Field>
)
}Props
Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».
Field
Hereda las props de Field.Root.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
name | string | — | Heredada de Base UI. Identifica al campo en los valores del submit y en el objeto errors de Form. |
validate | ((value: unknown, formValues: Form.Values) => string | string[] | void | Promise<string | string[] | void>) | — | Heredada de Base UI. Devolvé el mensaje si el valor está mal, o null si está bien. Puede ser asíncrona. |
validationMode | "onBlur" | "onChange" | "onSubmit" | — | Heredada de Base UI. onSubmit (default), onBlur u onChange. Tiene precedencia sobre el del Form. |
FieldControl
Hereda las props de Field.Control.
Sin props propias: pasa todo al primitivo.
FieldDescription
Hereda las props de Field.Description.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
FieldError
Hereda las props de Field.Error.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
alert | boolean | false | role="alert" para que el error se anuncie al aparecer. Solo con validationMode="onChange". |
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
FieldLabel
Hereda las props de Field.Label.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
indicator | boolean | false | El asterisco más «, obligatorio» para el lector (labels.field.required), sin la restricción nativa: para un campo que valida el servidor. No va con un control required o aria-required, que lo diría dos veces. |
required | boolean | false | Dibuja el asterisco. No hace obligatorio al campo: eso es el required del control. |
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
FieldValidity
Hereda las props de Field.Validity.
Sin props propias: pasa todo al primitivo.
Teclado
- Tab
- Entra y sale del control. La etiqueta no recibe foco: al clickearla, lo recibe el control.
Accesibilidad
- Esto es lo que resuelve el componente. La etiqueta nombra al control, la ayuda y el error lo describen, y el error pone
aria-invalid: todo por anidar las partes, sinuseId, sinhtmlFory sin armar unaria-describedbycondicional que es justo lo que se olvida. - El asterisco de
requiredesaria-hidden: nadie escucha "Razón social asterisco". Que el campo sea obligatorio lo anuncia elrequireddel control. - Si lo valida el servidor y el control no lleva
required,FieldLabel indicator: el mismo asterisco, y el nombre del control pasa a ser «Razón social, obligatorio» (el texto eslabels.field.required). Conrequireden la etiqueta también, ese texto se calla: lo anuncia elrequireddel control. El del control solo no se detecta:indicatorva sinrequiredniaria-requireden el control, o se diría dos veces. FieldErrorno ocupa lugar mientras el campo está bien, y cuando aparece ya está referenciado: no hace falta mover el foco para que se lea.- Escribí siempre el mensaje, con `match` o con `validate`. El del navegador sale en el idioma del navegador y no en el de la página: con Chrome en inglés, abajo de «Razón social» aparece «Please fill out this field».
match="valueMissing"y compañía además hablan del dato —«Falta la razón social»— en vez del input. - El mensaje se lee al enfocar el campo, porque el campo lo referencia con
aria-describedby. ConvalidationMode="onChange"eso no alcanza: el error aparece con el foco ya adentro y nada lo anuncia. Para ese caso estáalert, que le ponerole="alert". Es opt-in porquerole="alert"interrumpe: en el camino de enviar duplicaría el anuncio y cortaría el del nombre del campo, que es la mitad que da contexto. Input,TextareaySelectse enganchan solos. Para cualquier otro control vaFieldControlconrender.- Un control propio se engancha solo si reenvía lo que recibe.
FieldControlle pasaid,name,aria-describedby,aria-invalidy unaref; un componente que declaraidynamecomo props propias y no hace spread del resto se queda sin nada, y la etiqueta del campo apunta al vacío. Pasa seguido con un date picker o un autocomplete hechos con un<input type="hidden">más un botón: el arreglo va adentro de ese componente, no en cada uso.
Reglas de uso
- El `name` es la bisagra con `Form`: es la clave de los valores del submit y la del objeto
errorsque devuelve el servidor. Para datos anidados se usa punto (domicilio.calle), que es lo que devuelve el adaptador de schemas. - `validationMode="onSubmit"` (el default) casi siempre. Marcar el email en rojo mientras se escribe es castigar a alguien por no haber terminado.
onBlurpara un dato que recién se puede juzgar completo;onChangesolo cuando se puede evaluar desde el primer carácter, como el largo de una contraseña. - La ayuda va visible en
FieldDescription, no en un tooltip: una ayuda que hay que descubrir no ayuda a quien más la necesita. - Un mensaje propio para un motivo puntual se escribe con
match(<FieldError match="valueMissing">Falta el email</FieldError>): habla del dato, no del input. - Validar contra Zod, Valibot o ArkType:
fieldValidatordesebs7n-ui/lib/schema. - `FieldError` es una fuente o la otra, no las dos. Sin
matchse muestra ante *cualquier* invalidez —la del navegador y la que vino del servidor—, así que puesto al lado de uno conmatchimprime el mensaje dos veces. Si escribís mensajes propios conmatch, renderizá el genérico solo cuando el servidor devolvió algo para ese campo. - Un mensaje propio por
matchno es adorno: el del navegador sale en el idioma del navegador, no en el de la página, y habla del input ("complete este campo") en vez del dato que se pide.