Input
A single-line field that shows focus, error, and disabled states as you type. Pair with field for label and helper.
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
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| error | boolean | false | `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>
| Clase | Propósito |
|---|---|
input-group | root |
input-group--disabled | modifier |
input-group--error | modifier |
input-group__icon | element |
input-group__input | 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 | bg | none | (unparsed) |
| all | border | none | (unparsed) |
| all | focused-border | #171717 | border.focus |
| all | error-border | #f84131 | destructive |
| all | disabled-bg | #f5f5f5 | muted |
| all | disabled-border | #f5f5f5 | muted |
| all | fg | #525252 | placeholder |
| all | disabled-fg | #525252 | muted.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
border-color | var(--duration-150) | var(--easing-out) |
box-shadow | var(--duration-150) | var(--easing-out) |
border-color | var(--duration-150) | var(--easing-out) |
box-shadow | var(--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
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';