Field
Wraps a control with label, helper, and error so the whole row stays aligned. Required around every form input.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Labeled control with error:
```tsx import { Field } from '@/components/atoms/Field'; import { Input } from '@/components/atoms/Input';
<Field label="Email" htmlFor="email" error={err} required> <Input id="email" type="email" error={!!err} /> </Field> ```
## Accesibilidad
- Pass `htmlFor` matching the child control `id`, or wrap a single native control so the label associates. - `error` renders with `role="alert"`; prefer one clear message, not duplicated helper + error at once.
### Correcto
- role='group' en el root agrupa label + control + helper semanticamente - htmlFor conecta el label con el input — click en label enfoca el input - field__error tiene role='alert' — screen readers lo anuncian inmediatamente al aparecer - data-invalid marca el grupo completo como invalido para CSS y assistive tech - Required asterisco es visual + el input debe tener required attribute para a11y real
### Evitar
- No usar Field sin htmlFor si el control tiene id — la conexion label-input se pierde - No depender solo del color rojo para comunicar error — el mensaje de texto es obligatorio - No olvidar pasar `error={true}` al Input/Textarea ademas de error='msg' al Field — son independientes
## Cuándo no usar
- Decorative headings above non-form UI — use Typography, not Field. - Field does not render the input itself — always provide `Input` / `Select` / `Textarea` / etc. as children.
## Criterio de uso
- Usa `Field` como la unidad semántica de un control: label, ayuda, error y estado deben permanecer alineados. - Marca `required` sólo cuando el envío realmente dependa del valor y valida igualmente en código; el asterisco no sustituye la validación. - Presenta un solo mensaje de error accionable y evita duplicar la misma información en helper, tooltip y error.
## Gotchas
- `Field` no crea el control: el hijo debe tener un `id` asociado al label o estar correctamente envuelto. - `disabled` debe reflejar una razón de producto entendible; si el campo está bloqueado por plan, explica cómo habilitarlo. - **Nota**: Field no tiene tokens de componente propios — usa solo tokens semanticos (foreground, destructive, muted-foreground, placeholder, spacing-1, font-size-xs). - **Nota**: CSS autocontenido. Field es puro layout — no tiene transiciones, animaciones ni estados hover. Requiere el CSS del control hijo (Input, Textarea, etc.) por separado.
## Notas de diseño
---
`<Callout type="info">` Field no pasa props a sus children automaticamente. Si necesitas que el Input tenga error styling, pasale error=\{true} directamente al Input ademas de pasar el mensaje a Field. `</Callout>`
Uso
import { Field } from '@/components/atoms/Field';
import { Input } from '@/components/atoms/Input';
<Field label="Email" htmlFor="email" error={err} required>
<Input id="email" type="email" error={!!err} />
</Field>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| required | boolean | false | `true` / `false` | Marks the field as required in the label UI. | true when the form blocks submit without this value; still validate in code. Default: false. |
| disabled | boolean | false | `true` / `false` | Disables label + child control affordances. | true when the field is locked by plan; prefer explaining why nearby. Default: false. |
Gotchas
- a11y
Pass htmlFor matching the control id, or wrap a single control so the label associates; put error text in error prop for aria-describedby patterns.
- react
Field does not render the input itself — children must be Input/Select/Textarea/etc.
Anatomía CSS
<div class="field"> <span class="field__description"></span> <span class="field__error"></span> <span class="field__label"></span> <span class="field__label--required"></span> </div>
| Clase | Propósito |
|---|---|
field | root |
field--disabled | modifier |
field__description | element |
field__error | element |
field__label | element |
field__label--required | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | fg | #f84131 | destructive |
| all | error-fg | #f84131 | destructive |
| all | disabled-fg | #525252 | muted.foreground |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Field
Wrapper for form controls: label + input + description + error.
Composes with Input, Textarea, Select, Checkbox, etc.
Colors matched to Figma atom-text-field.
Orientation: vertical (default)
------------------------------------------------------------------------- */
.field {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.field__label {
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
font-weight: 500;
line-height: 1;
color: #0a0a0a;
}
.field__label--required::after {
content: " *";
color: #f84131;
}
.field__description {
margin-top: 4px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
line-height: 1.4;
color: #525252;
}
.field__error {
margin-top: 4px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
line-height: 1.4;
color: #f84131;
}
/* Error state — label turns red */
.field[data-invalid] .field__label {
color: #f84131;
}
/* Disabled state — label grays out */
.field--disabled .field__label {
color: #525252;
}
Codigo fuente
import { cloneElement, isValidElement, useId, type ReactNode } from 'react'; export type FieldProps = { label?: string; description?: string; error?: string; required?: boolean; disabled?: boolean; htmlFor?: string; children: ReactNode; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export function Field({ label, description, error, required = false, disabled = false, htmlFor, children, className, }: FieldProps) { const id = useId(); // El error sustituye a la descripcion en el render, asi que solo se referencia // el texto que de verdad esta en el DOM: apuntar a un id ausente deja al lector // de pantalla sin anunciar nada. const descriptionId = description && !error ? `${id}-description` : undefined; const errorId = error ? `${id}-error` : undefined; const describedBy = [descriptionId, errorId].filter(Boolean).join(' ') || undefined; // Solo se anota el control cuando Field recibe un unico elemento: con varios // hijos no hay forma de saber cual es el campo, y describir el equivocado es // peor que no describir nada. const describedChildren = describedBy && isValidElement<{ 'aria-describedby'?: string }>(children) ? cloneElement(children, { 'aria-describedby': [children.props['aria-describedby'], describedBy] .filter(Boolean) .join(' '), }) : children; return ( <div className={cn('field', disabled && 'field--disabled', className)} role="group" {...(error ? { 'data-invalid': '' } : {})} > {label && ( <label className={cn('field__label', required && 'field__label--required')} htmlFor={htmlFor} > {label} </label> )} {describedChildren} {description && !error && ( <p className="field__description" id={descriptionId}>{description}</p> )} {error && ( <p className="field__error" id={errorId} role="alert">{error}</p> )} </div> ); }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Field) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.field__label--required::after— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.field[data-invalid] .field__label— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.field--disabled .field__label— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
Tras pegar: Create component → nombre Atom / Field → Publish.