ATOM
Components (registry)Forms

Field

Wraps a control with label, helper, and error so the whole row stays aligned. Required around every form input.

Preview

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

PropTipoDefaultRango / opcionesWhatHow
requiredbooleanfalse`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.
disabledbooleanfalse`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>
ClasePropósito
fieldroot
field--disabledmodifier
field__descriptionelement
field__errorelement
field__labelelement
field__label--requiredelement

Tokens resueltos

Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.

VariantePropValorToken
allfg#f84131destructive
allerror-fg#f84131destructive
alldisabled-fg#525252muted.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

components-react
components/atoms/Field.tsx
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

Webflowfield

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)

  • selector on .field__label--required::aftercompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .field[data-invalid] .field__labelcompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .field--disabled .field__labelcompound/descendant selector — moved to head Custom Code (Designer styles are single-class)

Tras pegar: Create component → nombre Atom / Field → Publish.

On this page

Detalles

Publicado14 de mayo de 2026
Categoriaforms
Lectura...
Visitas...
Ayuda?Slack

Componente

Field

Source

components-react / cssDisponible via MCP: atom_uikit_source("field")