ATOM
Components (registry)Forms

Input

A single-line field that shows focus, error, and disabled states as you type. Pair with field for label and helper.

Preview

Editorial

<!-- F12c editorial — non-derivable only. Review: Karen. -->

## Ejemplos

```tsx import { Input } from '@/components/atoms/Input'; import { Field } from '@/components/atoms/Field';

<Field label="Email" error="Required"> <Input type="email" error placeholder="you@company.com" /> </Field> ```

## Accesibilidad

- Pair with `Field` (label + helper/error) so the control has an accessible name and `aria-describedby`. - `iconLeft` / `iconRight` are ReactNode slots — do not put interactive buttons inside without keyboard support.

### Correcto

- aria-invalid='true' se agrega automaticamente cuando `error={true}` - `disabled={true}` remueve del tab order y anuncia como no disponible - Focus ring visible (box-shadow 2px) para navegacion por teclado - Error ring rojo distinto del focus ring gris — distinguibles visualmente - Usa Input dentro de Field para label + mensaje de error asociados - placeholder no reemplaza un `<label>` — siempre usa label via Field

### Evitar

- No usar placeholder como unico indicador del campo — desaparece al escribir - No usar error sin mensaje explicativo (usa Field con helperText) - No cambiar el height — 40px coincide con Button m para alineacion en forms

## Cuándo no usar

- Multi-line free text → `Textarea`. - Choosing from a fixed list → `Select` / `Combobox`.

## Criterio de uso

- Úsalo para valores de una sola línea y acompáñalo siempre de `Field` cuando necesite label, ayuda o error. - Activa `error` sólo después de una validación fallida y muestra el motivo junto al control; el borde por sí solo no explica qué corregir. - Reserva `iconLeft` y `iconRight` para información o affordances no interactivas. Una acción dentro del campo necesita su propio foco y nombre accesible.

## Gotchas

- El `id` del input debe coincidir con `htmlFor` del label y los mensajes de `Field` deben quedar asociados mediante descripción accesible. - No uses placeholder como sustituto del label: desaparece al escribir y no comunica el propósito de forma persistente. - **Nota**: Input height (40px) coincide con Button size='m' (40px). Siempre usa el mismo tamano de boton que de input en un form. - **Nota**: CSS autocontenido. El input-group es el wrapper para iconos — si no usas iconos, solo necesitas .input.

Uso

import { Input } from '@/components/atoms/Input';
import { Field } from '@/components/atoms/Field';

<Field label="Email" error="Required">
  <Input type="email" error placeholder="you@company.com" />
</Field>

Props

PropTipoDefaultRango / opcionesWhatHow
errorbooleanfalse`true` / `false`Shows the error surface (border/color) on the field.true only when validation failed and a helper/error message is visible nearby. Default: false.

Gotchas

  • a11y

    Pair with Field (label + helper/error) so the control has an accessible name and describedby.

  • react

    iconLeft/iconRight are ReactNode slots — do not put interactive buttons inside without keyboard support.

Anatomía CSS

<div class="input-group">
  <span class="input-group__icon"></span>
  <span class="input-group__input"></span>
</div>
ClasePropósito
input-grouproot
input-group--disabledmodifier
input-group--errormodifier
input-group__iconelement
input-group__inputelement

Tokens resueltos

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

VariantePropValorToken
allbgnone(unparsed)
allbordernone(unparsed)
allfocused-border#171717border.focus
allerror-border#f84131destructive
alldisabled-bg#f5f5f5muted
alldisabled-border#f5f5f5muted
allfg#525252placeholder
alldisabled-fg#525252muted.foreground

Animaciones

PropiedadDuraciónEasing
border-colorvar(--duration-150)var(--easing-out)
box-shadowvar(--duration-150)var(--easing-out)
border-colorvar(--duration-150)var(--easing-out)
box-shadowvar(--duration-150)var(--easing-out)

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).

/* -------------------------------------------------------------------------
   Input

   Base text input. Height matches Button m (40px).
   Used standalone or inside Field wrapper.
   Colors matched to Figma atom-text-field.

   States: default, focus, disabled, error
   ------------------------------------------------------------------------- */

.input {
  display: flex;
  width: 100%;
  height: 2.5rem;
  padding: 0 8px;
  border: 1.5px solid #d4d4d4;
  border-radius: 8px;
  background-color: #fafafa;
  color: #0a0a0a;
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
  font-size: 12.8px;
  line-height: 1;
  outline: none;
  transition:
    border-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.input::placeholder {
  color: #525252;
}

.input:focus {
  border-color: #171717;
  box-shadow: 0 0 0 2px var(--focus-ring-color);
}

.input:disabled {
  background-color: #f5f5f5;
  border-color: #f5f5f5;
  color: #525252;
  cursor: not-allowed;
  opacity: 1;
}

.input[aria-invalid="true"] {
  border-color: #f84131;
  box-shadow: 0 0 2px 0 var(--focus-ring-error-color);
}

.input[aria-invalid="true"]:focus {
  border-color: #f84131;
  box-shadow: 0 0 0 2px var(--focus-ring-error-color);
}

/* File input */
.input[type="file"] {
  padding: 0;
}

.input[type="file"]::file-selector-button {
  height: 100%;
  padding: 0 12px;
  border: none;
  border-right: 1.5px solid #d4d4d4;
  background-color: #f5f5f5;
  color: #0a0a0a;
  font-family: inherit;
  font-size: inherit;
  font-weight: 500;
  cursor: pointer;
}

/* ---- Input Group (input with leading/trailing icons) ---- */

.input-group {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 2.5rem;
  padding: 0 8px;
  border: 1.5px solid #d4d4d4;
  border-radius: 8px;
  background-color: #fafafa;
  transition:
    border-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.input-group:focus-within {
  border-color: #171717;
  box-shadow: 0 0 0 2px var(--focus-ring-color);
}

.input-group--error {
  border-color: #f84131;
  box-shadow: 0 0 2px 0 var(--focus-ring-error-color);
}

.input-group--error:focus-within {
  border-color: #f84131;
  box-shadow: 0 0 0 2px var(--focus-ring-error-color);
}

.input-group--disabled {
  background-color: #f5f5f5;
  border-color: #f5f5f5;
  cursor: not-allowed;
}

.input-group__input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: #0a0a0a;
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
  font-size: 12.8px;
  line-height: 1;
  outline: none;
  padding: 0;
}

.input-group__input::placeholder {
  color: #525252;
}

.input-group__input:disabled {
  color: #525252;
  cursor: not-allowed;
}

.input-group__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1em;
  height: 1em;
  color: #525252;
}

.input-group__icon svg {
  width: 100%;
  height: 100%;
}

.input-group--disabled .input-group__icon {
  color: #525252;
}

Codigo fuente

components-react
components/atoms/Input.tsx
import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react';

export type InputProps = {
  error?: boolean;
  iconLeft?: ReactNode;
  iconRight?: ReactNode;
  className?: string;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'className'>;

function cn(...classes: (string | false | undefined | null)[]) {
  return classes.filter(Boolean).join(' ');
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ error = false, iconLeft, iconRight, className, ...props }, ref) => {
    const hasIcons = iconLeft || iconRight;

    if (hasIcons) {
      return (
        <div
          className={cn(
            'input-group',
            error && 'input-group--error',
            props.disabled && 'input-group--disabled',
            className,
          )}
        >
          {iconLeft && <span className="input-group__icon">{iconLeft}</span>}
          <input
            ref={ref}
            className="input-group__input"
            aria-invalid={error || undefined}
            {...props}
          />
          {iconRight && <span className="input-group__icon">{iconRight}</span>}
        </div>
      );
    }

    return (
      <input
        ref={ref}
        className={cn('input', className)}
        aria-invalid={error || undefined}
        {...props}
      />
    );
  },
);

Input.displayName = 'Input';

Componentes relacionados

On this page

Detalles

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

Componente

Input

Source

components-react / cssDisponible via MCP: atom_uikit_source("input")
Abrir en Storybook