Saltar al contenido principal

Field

Label, control, texto de ayuda y errores compuestos en un campo de formulario accesible. Vertical, horizontal o responsive.

Shown in the sidebar and in deploy notifications.

Optional. Markdown is supported.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Field } from '@/components/ui/field';
5import { Input } from '@/components/ui/input';
6import { Textarea } from '@/components/ui/textarea';
7
8export function Default() {
9 return (
10 <form className="border-border bg-card w-full max-w-sm rounded-xl border p-6">
11 <Field.Group>
12 <Field>
13 <Field.Label>Project name</Field.Label>
14 <Field.Control>
15 <Input placeholder="Checkout redesign" />
16 </Field.Control>
17 <Field.Description>Shown in the sidebar and in deploy notifications.</Field.Description>
18 </Field>
19 <Field>
20 <Field.Label>Description</Field.Label>
21 <Field.Control>
22 <Textarea placeholder="What is this project for?" rows={3} />
23 </Field.Control>
24 <Field.Description>Optional. Markdown is supported.</Field.Description>
25 </Field>
26 <Field orientation="horizontal">
27 <Button type="button" variant="outline" size="sm">
28 Cancel
29 </Button>
30 <Button type="button" size="sm">
31 Create project
32 </Button>
33 </Field>
34 </Field.Group>
35 </form>
36 );
37}

Instalación

pnpm dlx nachui add field

Anatomía

1import { Field } from '@/components/ui/field';
1<Field invalid={hasError}>
2 <Field.Label>Email</Field.Label>
3 <Field.Control>
4 <Input type="email" />
5 </Field.Control>
6 <Field.Description>Solo lo usamos para los recibos.</Field.Description>
7 <Field.Error errors={errors} />
8</Field>
Field genera un id y lo comparte con sus partes. Label apunta a él, Description y Error registran sus ids, y Control clona el elemento que tiene adentro con id, aria-describedby, aria-invalid y disabled. Funciona con cualquier cosa que renderice un control nativo: Input, Textarea, Select, Switch, Checkbox, Radio o un elemento plano. Para un componente que no puede recibir un hijo, llama a useField() y esparce el resultado.

Composición

Field.Set
├── Field.Legend
├── Field.Description
└── Field.Group
├── Field
│ ├── Field.Label
│ ├── Field.Control
│ ├── Field.Description
│ └── Field.Error
├── Field.Separator
└── Field
├── Field.Content
│ ├── Field.Label
│ └── Field.Description
└── Field.Control
Group separa los campos de un formulario. Set y Legend agrupan controles relacionados en un fieldset real. Content mantiene juntos un label y su descripción cuando el control va al lado.

Variantes

Predeterminado (Default)

Un formulario vertical: label, control, descripción.

Shown in the sidebar and in deploy notifications.

Optional. Markdown is supported.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Field } from '@/components/ui/field';
5import { Input } from '@/components/ui/input';
6import { Textarea } from '@/components/ui/textarea';
7
8export function Default() {
9 return (
10 <form className="border-border bg-card w-full max-w-sm rounded-xl border p-6">
11 <Field.Group>
12 <Field>
13 <Field.Label>Project name</Field.Label>
14 <Field.Control>
15 <Input placeholder="Checkout redesign" />
16 </Field.Control>
17 <Field.Description>Shown in the sidebar and in deploy notifications.</Field.Description>
18 </Field>
19 <Field>
20 <Field.Label>Description</Field.Label>
21 <Field.Control>
22 <Textarea placeholder="What is this project for?" rows={3} />
23 </Field.Control>
24 <Field.Description>Optional. Markdown is supported.</Field.Description>
25 </Field>
26 <Field orientation="horizontal">
27 <Button type="button" variant="outline" size="sm">
28 Cancel
29 </Button>
30 <Button type="button" size="sm">
31 Create project
32 </Button>
33 </Field>
34 </Field.Group>
35 </form>
36 );
37}

Horizontal

orientation="horizontal" alinea las partes en una fila. Pon el label y la descripción dentro de Content para que se apilen y el control quede al final.

Post to the team channel when a production deploy finishes.

Page the on-call engineer when an alert fires.

A summary of usage and spend every Monday.

1'use client';
2
3import { Field } from '@/components/ui/field';
4import { Switch } from '@/components/ui/switch';
5
6const settings = [
7 {
8 id: 'notify-deploys',
9 title: 'Deploy notifications',
10 description: 'Post to the team channel when a production deploy finishes.',
11 defaultChecked: true,
12 },
13 {
14 id: 'notify-incidents',
15 title: 'Incident pages',
16 description: 'Page the on-call engineer when an alert fires.',
17 defaultChecked: true,
18 },
19 {
20 id: 'notify-digest',
21 title: 'Weekly digest',
22 description: 'A summary of usage and spend every Monday.',
23 defaultChecked: false,
24 },
25];
26
27export function Horizontal() {
28 return (
29 <Field.Group className="w-full max-w-sm gap-5">
30 {settings.map((setting) => (
31 <Field key={setting.id} orientation="horizontal" id={setting.id}>
32 <Field.Content>
33 <Field.Label>{setting.title}</Field.Label>
34 <Field.Description>{setting.description}</Field.Description>
35 </Field.Content>
36 <Field.Control>
37 <Switch defaultChecked={setting.defaultChecked} />
38 </Field.Control>
39 </Field>
40 ))}
41 </Field.Group>
42 );
43}

Fieldset

Opciones de radio o checkbox agrupadas bajo una leyenda. Cada opción es su propio Field, así el label activa el control.
Plan

You can change this at any time from billing.

Free, one project, community support

$20 per user, unlimited projects

$48 per user, SSO and audit log

1'use client';
2
3import { Field } from '@/components/ui/field';
4import { Radio } from '@/components/ui/radio';
5
6const plans = [
7 { value: 'hobby', title: 'Hobby', detail: 'Free, one project, community support' },
8 { value: 'pro', title: 'Pro', detail: '$20 per user, unlimited projects' },
9 { value: 'team', title: 'Team', detail: '$48 per user, SSO and audit log' },
10];
11
12export function Fieldset() {
13 return (
14 <Field.Set className="w-full max-w-sm">
15 <Field.Legend>Plan</Field.Legend>
16 <Field.Description className="-mt-4">
17 You can change this at any time from billing.
18 </Field.Description>
19 <Field.Group className="gap-3">
20 {plans.map((plan) => (
21 <Field
22 key={plan.value}
23 orientation="horizontal"
24 id={`plan-${plan.value}`}
25 className="border-border bg-card rounded-lg border p-3"
26 >
27 <Field.Control>
28 <Radio name="plan" value={plan.value} defaultChecked={plan.value === 'pro'} />
29 </Field.Control>
30 <Field.Content>
31 <Field.Label>{plan.title}</Field.Label>
32 <Field.Description>{plan.detail}</Field.Description>
33 </Field.Content>
34 </Field>
35 ))}
36 </Field.Group>
37 </Field.Set>
38 );
39}

Con error

Pasa invalid a la raíz y los errores a Error. Acepta strings, objetos con message y valores vacíos, así que el arreglo de errores de una librería de formularios funciona tal cual. Un mensaje se muestra como texto, varios como lista.

northwind.app/My Team!

1'use client';
2
3import * as React from 'react';
4import { Button } from '@/components/ui/button';
5import { Field } from '@/components/ui/field';
6import { Input } from '@/components/ui/input';
7
8function validate(value: string): string[] {
9 const errors: string[] = [];
10 if (value.length < 3) errors.push('Use at least 3 characters.');
11 if (/[^a-z0-9-]/.test(value)) errors.push('Only lowercase letters, numbers and dashes.');
12 if (value.startsWith('-') || value.endsWith('-')) errors.push('Cannot start or end with a dash.');
13 return errors;
14}
15
16export function WithError() {
17 const [slug, setSlug] = React.useState('My Team!');
18 const errors = validate(slug);
19
20 return (
21 <form
22 className="border-border bg-card w-full max-w-sm rounded-xl border p-6"
23 onSubmit={(event) => event.preventDefault()}
24 >
25 <Field.Group>
26 <Field invalid={errors.length > 0}>
27 <Field.Label>Workspace URL</Field.Label>
28 <Field.Control>
29 <Input value={slug} onChange={(event) => setSlug(event.target.value)} />
30 </Field.Control>
31 <Field.Description>northwind.app/{slug || 'your-workspace'}</Field.Description>
32 <Field.Error errors={errors} />
33 </Field>
34 <Field orientation="horizontal">
35 <Button type="submit" size="sm" disabled={errors.length > 0}>
36 Save
37 </Button>
38 </Field>
39 </Field.Group>
40 </form>
41 );
42}

Responsive

orientation="responsive" es vertical por defecto y pasa a horizontal cuando el Field.Group que lo contiene mide al menos 28rem. Usa una container query, así que sigue al formulario y no al viewport.

Hook

1import { useField } from '@/components/ui/field';
2
3function ColorPicker() {
4 const field = useField();
5 return <input type="color" {...field} />;
6}
useField devuelve id, aria-describedby, aria-invalid y disabled del Field más cercano.

Referencia de API

Field

PropTipoPor defectoDescripción
orientation"vertical" | "horizontal" | "responsive""vertical"Layout de las partes
invalidbooleanfalseMarca el campo y su control como inválidos
disabledbooleanfalseAtenúa el campo y deshabilita el control
idstringgeneradoId que recibe el control
namestring-Se guarda como data-name para estilos y tests
classNamestring-Clases CSS adicionales

Field.Control

PropTipoPor defectoDescripción
childrenReactElement-El único elemento que recibe las props

Field.Error

PropTipoPor defectoDescripción
errorsArray<string | { message?: string } | null>-Mensajes a mostrar. Los duplicados se descartan
childrenReactNode-Contenido propio en lugar de errors

Field.Legend

PropTipoPor defectoDescripción
variant"legend" | "label""legend"Título grande o tamaño de label

Field.Label, Field.Description, Field.Content, Field.Title, Field.Set, Field.Group, Field.Separator

Partes de presentación. Cada una acepta className y los atributos del elemento que renderiza. Separator acepta hijos opcionales que se muestran en el medio de la línea.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue
Ctrl+I