# Input

A single-line text field: a sunken well with an optional icon at either end, room for content before the value and an action after it, in three sizes.

Block: `smbk-input`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `lg`, `md`, `sm` | `md` | The control height, with its padding, icons and text: md for forms, lg beside large buttons (a sign-in or search form on a marketing page), sm for dense tables and toolbars. Small text never goes under 16 px on a phone. |
| `error` | on / off | off | The value is invalid. Draws the danger border, kept while focused, and the error icon; the control also carries aria-invalid. |

Parts: `smbk-input__control`, `smbk-input__icon`, `smbk-input__error-icon`, `smbk-input__leading-content`, `smbk-input__trailing-action`

## Markup

```html
<span class="smbk-input smbk-input--size-md">
  <input class="smbk-input__control" placeholder="Email address" aria-label="Email address"/>
  <i class="ph ph-at smbk-icon smbk-icon--size-md smbk-input__icon" aria-hidden="true"></i>
</span>
```

## React

```tsx
import { Input } from './design-system/components/input/input.js';

<Input size="md" placeholder="Email address" aria-label="Email address" trailingIcon="At" />
```

## Stylesheet

```css
/* smbk-input — Input. Figma "Input" 4212:26450.
 * The block is the well; the native control sits inside it with no chrome of
 * its own, so that icons and the control share one border, one focus ring.
 * The search field and the select are built on this well: their markup
 * carries smbk-input, so every change here reaches them too. The select's
 * well is itself the control, a button, which the focus, read-only and
 * disabled rules below also cover. The one-time code input draws each of its
 * cells as this well, with one native control over them all: the focus,
 * read-only and disabled rules below name its cells too, the cell marked
 * data-active taking the focus while the control has it. */

.smbk-input {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: var(--smbk-space-inline-md);
  width: 100%;
  height: var(--smbk-size-control-md);
  padding: 0 var(--smbk-space-inset-lg);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-rim);
  border-radius: var(--smbk-radius-control-md);
  background: var(--smbk-color-surface-sunken);
  color: var(--smbk-color-text-default);
  box-shadow: var(--smbk-elevation-inset-strong);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
  cursor: text;
}

/* Hover lifts the well onto the raised surface; the edge highlight stays. */
@media (hover: hover) {
  .smbk-input:hover {
    background: var(--smbk-color-surface-raised);
    box-shadow: var(--smbk-elevation-control);
  }
}

/* Focus: the field's border. A well that is itself a button (the select's) shows it only when the keyboard
   brought focus there, as every button does: a press opens its list, and a choice hands focus back to it
   without the focused look. A field typed into shows it however it was reached. */
.smbk-input:focus-within:where(:not(button:not(:focus-visible))),
:where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control:focus)) .smbk-input[data-active] {
  background: var(--smbk-color-surface-sunken);
  border-color: var(--smbk-color-border-focus-field);
  box-shadow: var(--smbk-elevation-inset-strong);
}

/* Field focus is the border alone; a well that is itself the control draws no ring. */
.smbk-input:focus-visible {
  outline: none;
}

/* In error the danger edge stays while the field has focus and under the pointer, with the error icon
   beside the value, so the error is a shape as well as a color. Two classes, so it wins over focus. A well or a
   control in it marked aria-invalid="true" takes the same edge, at the same weight, for markup written by hand;
   the error icon is markup. */
.smbk-input.smbk-input--error,
.smbk-input[aria-invalid="true"],
.smbk-input:has([aria-invalid="true"]) {
  border-color: var(--smbk-color-feedback-danger-border);
}

/* Read-only: the value can be read, selected and copied, not changed. The well sits flat on the page's
   surface with a faint edge, no shadow and no lift on hover; it stays in the tab order and shows focus by
   its border, as every field does. The select's well, a button, says it with aria-readonly. */
.smbk-input:has(.smbk-input__control[readonly]),
.smbk-input[aria-readonly="true"],
:where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control[readonly])) .smbk-input.smbk-one-time-code-input__cell {
  border-color: var(--smbk-color-border-rim-subtle);
  background: var(--smbk-color-surface-default);
  box-shadow: none;
}

.smbk-input:has(.smbk-input__control[readonly]):focus-within,
.smbk-input[aria-readonly="true"]:focus-within:where(:not(button:not(:focus-visible))),
:where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control[readonly]:focus)) .smbk-input.smbk-one-time-code-input__cell[data-active] {
  border-color: var(--smbk-color-border-focus-field);
}

.smbk-input:has(.smbk-input__control:disabled),
.smbk-input:disabled,
:where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control:disabled)) .smbk-input.smbk-one-time-code-input__cell {
  border-color: transparent;
  background: var(--smbk-color-surface-disabled);
  color: var(--smbk-color-text-disabled);
  box-shadow: none;
  cursor: not-allowed;
}

.smbk-input__control {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  outline: none;
}

.smbk-input__control::placeholder {
  color: var(--smbk-color-text-subtlest);
  opacity: 1;
}

.smbk-input__control:disabled {
  color: var(--smbk-color-text-disabled);
  cursor: not-allowed;
}

/* Icons at either end are one size, the field's own: Figma draws the leading and trailing icon alike. */
.smbk-input__icon {
  color: var(--smbk-color-text-subtlest);
  font-size: var(--smbk-size-icon-md);
}

.smbk-input:has(.smbk-input__control:disabled) .smbk-input__icon {
  color: var(--smbk-color-text-disabled);
}

/* The error icon, Figma's Warning glyph, sits after the value and before any trailing icon. */
.smbk-input__error-icon {
  color: var(--smbk-color-feedback-danger-icon);
  font-size: var(--smbk-size-icon-md);
}

/* Figma's Leading content and Trailing action slots: what a field needs before the value (a swatch, a unit)
   and an action after it (a small button). Each is drawn only when given, so an empty one takes no room. A
   button inside keeps its own size and its own focus ring. */
.smbk-input__leading-content,
.smbk-input__trailing-action {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
}

.smbk-input__leading-content {
  gap: var(--smbk-space-inline-xs);
}

/* ---- large (lg): 48 tall, beside large buttons ----
   The md field's text, icons and gap on the large control's height, with the next padding and corner step. */

.smbk-input--size-lg {
  height: var(--smbk-size-control-lg);
  padding: 0 var(--smbk-space-inset-xl);
  border-radius: var(--smbk-radius-control-lg);
}

/* ---- small (sm): the small recipe, and text in Field SM, which never
   goes under 16 px on a phone, so a phone's browser never zooms into the field ---- */

.smbk-input--size-sm {
  gap: var(--smbk-space-inline-sm);
  height: var(--smbk-size-control-sm);
  padding: 0 var(--smbk-space-inset-md);
  border-radius: var(--smbk-radius-control-sm);
  font-family: var(--smbk-typography-field-sm-font-family);
  font-size: var(--smbk-typography-field-sm-font-size);
  font-weight: var(--smbk-typography-field-sm-font-weight);
  line-height: var(--smbk-typography-field-sm-line-height);
  letter-spacing: var(--smbk-typography-field-sm-letter-spacing);
}

.smbk-input--size-sm .smbk-input__icon,
.smbk-input--size-sm .smbk-input__error-icon {
  font-size: var(--smbk-size-icon-sm);
}

/* ---- touch: the value's box takes the comfortable target ----
   On a touch screen the native control reaches out to the comfortable target above and below its line, with
   padding that takes the press and margins that give the room back, so a press anywhere across the well, or just
   past it, lands on the field. It has no fill or edge of its own and its line stays where it is, so nothing that
   is drawn moves. It is placed relatively, with no offset, so it takes its turn among the press areas round it in
   the order of the page, as they do. */

@media (pointer: coarse) {
  .smbk-input__control {
    position: relative;
    padding-block: max(0%, (var(--smbk-size-target-comfortable) - 1lh) / 2);
    margin-block: min(0%, (1lh - var(--smbk-size-target-comfortable)) / 2);
  }
}

/* ---- touch: buttons inside the well stop halfway to the value ----
   A button inside the well (a clear cross, a list's caret, a trailing action) takes its own invisible press area on a
   touch screen; right after the value, and beside another button, that area reaches only halfway into the well's gap,
   so a press beside the value never lands on the button. */

@media (pointer: coarse) {
  :is(.smbk-input__control, .smbk-combobox__values) + button::after,
  :is(.smbk-input__control, .smbk-combobox__values) + .smbk-input__trailing-action > button:first-child::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
  }

  .smbk-input > button:has(+ button)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
  }

  .smbk-input--size-sm > :is(.smbk-input__control, .smbk-combobox__values) + button::after,
  .smbk-input--size-sm > :is(.smbk-input__control, .smbk-combobox__values) + .smbk-input__trailing-action > button:first-child::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-sm) / -2);
  }

  .smbk-input--size-sm > button:has(+ button)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-sm) / -2);
  }
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  /* Field focus is the edge alone, as everywhere: here the edge turns Highlight and thickens by a
     focus ring's width, since a hairline color change does not show in forced colors. */
  .smbk-input:focus-within:where(:not(button:not(:focus-visible))),
  :where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control:focus)) .smbk-input[data-active] {
    border-color: Highlight;
    outline: var(--smbk-focus-ring-width) solid Highlight;
    outline-offset: 0;
  }

  .smbk-input:has(.smbk-input__control:disabled),
  .smbk-input:disabled,
  .smbk-input__control:disabled,
  :where(.smbk-one-time-code-input:has(> .smbk-one-time-code-input__control:disabled)) .smbk-input.smbk-one-time-code-input__cell {
    border-color: GrayText;
    color: GrayText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    },
    "inset": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      },
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    }
  },
  "size": {
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{size.48}}",
        "$description": "Height of a large control: 48 comfortable, 40 compact. A page may use it for its main call to action; apps use md. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "focus-field": {
        "$type": "color",
        "$value": "{{color.blue.700}}",
        "$description": "Keyboard focus ring on fields."
      },
      "rim-subtle": {
        "$type": "color",
        "$value": "{{color.alpha.white.20}}",
        "$description": "A fainter light rim."
      }
    },
    "surface": {
      "sunken": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.5}}",
        "$description": "Recessed wells: fields, tracks, code. See-through, so a well always shows one step below what it sits on: the page, a card or a panel."
      },
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Surface of a disabled card or field."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      }
    },
    "feedback": {
      "danger": {
        "border": {
          "$type": "color",
          "$value": "{{color.red.500}}",
          "$description": "Danger messages: edge; the edge of a field in error (3:1)."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "radius": {
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{radius.6}}",
        "$description": "Corners of controls, medium."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of controls, large."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "elevation": {
    "inset-strong": {
      "$type": "shadow",
      "$value": {
        "light": "inset -1px 2px 2px 0px #373d4333",
        "dark": "inset -1px 2px 2px 0px #0000004d"
      }
    },
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "typography": {
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "field-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": {
          "min": "16px",
          "max": "14px"
        },
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Field SM: the text inside a small field. 14 on a desktop, 16 on a phone, so iPhones do not zoom in (answer 11)."
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      }
    }
  }
}
```

## Purpose

A single-line text field. It is drawn as a shallow well pressed into the surface, with the field's chrome — border, focus color, icons — on a wrapper around the native control, so the control itself stays a plain text box that behaves like every other one in the browser.

## Anatomy

- The block, the well: sunken surface, hairline highlight edge, inset shadow, fixed height by size.
- The control part, the native text control, chromeless.
- The icon part, before or after the control. Optional. Both are the same size, the one that goes with the field's size.
- The error icon part, a warning sign after the value, shown only in error.
- The leading content part, after the leading icon and before the value: a swatch, a unit, an avatar. Optional, and drawn only when given, so an empty one takes no room.
- The trailing action part, last: an action on the value, such as a small ghost button to copy it. Optional, and drawn only when given.

## When to use

Any short free-text value: a name, an email, a number that is really a name such as a phone number or a postcode, a search term when the search field's own clear control is not wanted.

## When not to use

- Long text. Use the [text area](text-area.md).
- A choice from a list. Use the [select](select.md).
- Searching a list on the page. Use the [search field](search-field.md), which has the clear control.
- A password. Use the [password field](password-field.md), which can show what was typed.
- A quantity or a measure with limits and a step. Use the [number field](number-field.md).

## Behavior

Hover lifts the well to the raised surface. Focus draws the field focus color on the border, and nothing outside it: a field shows focus by its border alone, by design, with no ring. The error flag draws the danger border, which stays while the field has focus, adds the warning icon after the value, and marks the control invalid, so an error is a shape as well as a color. A read-only field sits flat on the page's surface with a faint edge and no shadow, and does not lift on hover; its value can be selected and copied, and it stays in the tab order. A disabled control grays the well and drops its edge and shadow.

The read-only and disabled looks follow the native readonly and disabled attributes on the control, so there is no class to set for either.

There are three sizes, lg, md and sm, 48, 40 and 32 tall. The large size keeps the medium text and icons on a taller field with more room at its sides, for a sign-in or search form that sits beside large buttons. The small size's text is 14 px from tablet width up and 16 px on a phone, so a phone's browser never zooms into the field when it takes focus. Every size fills the width of its space; a clear cross for the value goes in the trailing action.

What sits in the leading content and the trailing action is the page's: a button there keeps its own size and draws its own focus ring, and the well keeps its focus edge while focus is anywhere inside it.

A ref given to the wrapper reaches the native control, not the well, so a page can focus it, for example after a form is sent with the field empty. The text area and the search field do the same.

On a touch screen the field keeps its height, and the value's box reaches out to the comfortable touch target above and below its line, so a press anywhere across the well, or just past it, lands in the field. A button inside the well takes its own press area, reaching only halfway toward the value.

## Accessibility

### What it does for you

- It is a native text control inside a well, so typing, selecting and copying work as in any text field.
- The error flag sets `aria-invalid` on the control. The warning icon is decorative.
- A control (or the well itself) marked `aria-invalid="true"` draws the danger edge without the error class, so markup written by hand, or a page that marks its own errors, gets the error look; add the warning icon to the markup yourself. The checkbox, radio, choice card and file upload do the same.
- A read-only field keeps its value selectable and copyable and stays in the tab order.
- In development, an input on the page with no name logs a warning in the console.
- Icons are decorative.
- A button in the trailing action keeps its own size and focus ring, and is its own stop for the Tab key, after the control.
- On a touch screen the value's box reaches out to the comfortable touch target above and below its line, so a press across the well, or just past it, lands in the field.

### What you must do

- Give every input a label: the label component, associated by the `for` attribute, or an `aria-label` when the design hides it.
- Link the message that explains an error to the control with `aria-describedby`, from the form that shows it. The field component does this and sets the flag too.
- Use `readonly`, not `disabled`, for a value the reader may need to read or copy: a disabled control is skipped by Tab and often not read out.
- Give a button in the trailing action its own accessible name, such as "Copy the invite link".
- Say in the label too whatever the leading content tells the reader, since the leading content is not part of the field's name.
- Do not use placeholder text as a label: it disappears when typing starts.

### How to test

- Tab to it: the field's border changes to the focus color. With a trailing action, Tab again reaches the button, which shows its own focus ring.
- With a screen reader, the field is read with its label. In error, it is read as invalid, with the message as its description.
- A read-only field can be reached with Tab and its value copied. A disabled one is skipped.
- With text at 200% and the page 320 pixels wide, the value and the label stay readable.
- In a forced-color mode, the focus border and the text stay visible.
- On a touch screen, pressing just above or below the value's line puts the cursor in the field.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus into the control, then to a button in the trailing action. A disabled control is skipped; a read-only one is not. |
| Shift + Tab | Moves focus back the same way. |

## Usage rules

### Do

- Set the input type that matches the value, so the right keyboard appears.
- Use the small size only in dense tables and toolbars.
- Use the large size only beside large buttons, so a form's controls stand the same height.

### Don't

- Set a width on the control. Set it on the well, or let the field decide.
- Use placeholder text as the only label.
