Input

User Experience (UX)

How an input behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

Now let's look at how an input behaves for every person, input and setting. A good input is accessible, predictable, designed in every state and adaptive.

Accessible

Use the native element

A native <input> gives you focus, text editing, selection, copy and paste, undo, spellcheck, autofill, password managers, the right keyboard on phones and form submission, all for free. A contenteditable element only gives you the editing. It has no role or name, it isn't submitted with its form, autofill and password managers skip it, and phones can't tell which keyboard to show, so every one of those is one more thing to rebuild by hand.

{/* Do: you get editing, autofill, the right keyboard and form submission for free */}
<label htmlFor="email">Email address</label>
<input id="email" type="email" name="email" autoComplete="email" />

{/* Don't: there's no role, name, autofill or form submission until you rebuild them */}
<div className="input" contentEditable onInput={update} />

Label every input

The label is also the input's accessible name. Screen readers read it when the input gets focus, voice control users say it out loud ("click Phone number"), and tests and agents use it to find the input.

For international numbers, include the country code

<label htmlFor="phone">Phone number</label>
<p id="phone-hint">For international numbers, include the country code</p>
<Input id="phone" type="tel" autoComplete="tel" aria-describedby="phone-hint" />
  • Every input has a visible <label>, linked to it with for and the input's id (WCAG 1.3.1, 3.3.2). Clicking the label focuses the input too, which makes for a much bigger target.
  • A hint, like the format you expect, goes in the input's description, which screen readers read after its name. You can do that with aria-describedby, pointing at the hint's id.
  • A search input next to its Search button is the one input that doesn't need a label of its own. The button already says what it's for, so you can name the input with aria-label="Search".
  • If you name an input with aria-label or aria-labelledby, make sure the name includes the visible label, ideally at the start (WCAG 2.5.3).
  • The Input doesn't make up IDs of its own (see Deterministic), so give it an id for its label to point at, and one to each hint and error message that points back at it.

A placeholder isn't a label. It disappears as soon as people start typing, so they can't check what they were asked for, it's lighter than the text around it, so it's harder to read, and it isn't a reliable name for assistive technologies. If you use one at all, use it for an example, like name@example.com, and put anything people need to know in the hint.

{/* Do: the label stays put while people type */}
<label htmlFor="email">Email address</label>
<Input id="email" type="email" autoComplete="email" />

{/* Don't: the only label disappears as soon as people start typing */}
<Input type="email" autoComplete="email" placeholder="Email address" />

Contrast and size

  • The value needs a contrast of at least 4.5:1 with its background, and so do the placeholder, prefixes and suffixes, since people need to read them too (WCAG 1.4.3). Disabled inputs are exempt, but they should still be readable.
  • The border needs 3:1 against the page (WCAG 1.4.11). An empty input is nothing but its border, so it's how people see that there's something to fill in, and where to click. The focus ring needs 3:1 as well.
  • Every input is at least 24px tall (WCAG 2.5.8). On touch screens, the hit area grows to 44px tall (WCAG 2.5.5), and pressing anywhere on the field, its icons and padding included, focuses the input.

Show focus

When an input has focus, the whole field shows an outline in the same focus colour as every other component (WCAG 2.4.7). It's drawn by the root rather than the <input>, so it goes around the icons and buttons too, and like the Button's, it's at least 2px thick and drawn outside the border, and we recommend 2px for both. Browsers match :focus-visible on a text input however it got focus, since people are about to type either way, so the ring shows after a click too. A Button among the actions shows its own ring instead, so only one thing on the page ever looks focused.

You'll also want to keep focused inputs clear of sticky headers and footers, which you can do with scroll-padding (WCAG 2.4.11, 2.4.12). On phones, the on-screen keyboard covers the bottom of the page too, so check that a focused input near the bottom scrolls into view above it.

Predictable

Leave typing to the browser

The Input leaves text editing to the browser, so moving the caret, selecting, copying, cutting, pasting, undoing and spellchecking all work just as they do everywhere else. We recommend never taking any of them over, and never blocking paste. People paste addresses and codes, and password managers paste passwords in for them. Blocking paste makes people type a long password by hand, which WCAG counts as a cognitive function test (WCAG 3.3.8).

Keyboard

KeyInput
Tab, Shift+TabMoves focus to and from the input, and on to its actions
Arrows, Home and EndMove the caret, as in any other text field
EnterSubmits its form

Enter submits the form the input is in, through the form's submit button, which HTML calls implicit submission. Give every form a submit button, since a form with two inputs and no submit button doesn't submit at all, and don't make Enter do anything else, like move to the next input.

Pick the type that fits

The type tells phones which keyboard to show, and browsers what to expect. inputmode changes only the keyboard, which is what you need for digits that aren't amounts, like a security code.

<label htmlFor="email">Email address</label>
<Input id="email" type="email" autoComplete="email" spellCheck={false} />

<label htmlFor="phone">Phone number</label>
<Input id="phone" type="tel" autoComplete="tel" />

<label htmlFor="website">Website</label>
<Input id="website" type="url" autoComplete="url" spellCheck={false} />

<label htmlFor="code">Security code</label>
<Input id="code" inputMode="numeric" autoComplete="one-time-code" spellCheck={false} />
When people type…UsePhones showautocomplete, for example
A name, or any other texttype="text", the defaultThe standard keyboardname, given-name, organization
An email addresstype="email"A keyboard with @ and .email
A passwordtype="password"The standard keyboard, hiding the valuecurrent-password, new-password
A searchtype="search"A keyboard with a search keyNone
A phone numbertype="tel"A phone keypadtel
A web addresstype="url"A keyboard with / and .url
A code, or a card numbertype="text" with inputmode="numeric"A number keypadone-time-code, cc-number

We left number out of the Input's types on purpose. It's meant for amounts you'd step up and down, so browsers add arrows to it, accept an e for an exponent, and some change its value when you scroll over it. That's not what you want for a code, a card number or a phone number, so for those, use inputmode="numeric", which only changes the keyboard. Numbers with steppers, dates and the other types each get a component of their own.

Help people fill it in

  • Give each input that asks about the person filling in the form the matching autocomplete value, like email, tel or postal-code (WCAG 1.3.5). Browsers and password managers then fill it in for them, which helps everyone, and most of all people who find typing hard. You'll find every value in the HTML standard.
  • For values that aren't words, like usernames, codes and email addresses, turn off spellcheck, and autocapitalize and autocorrect where the type doesn't already, so phones don't capitalise the first letter or "correct" a username into a word.
  • If you'd like the Enter key on phones to say what it does, set enterkeyhint, as in enterkeyhint="search" or "send".

Don't cut people off

maxlength stops people from typing past a limit, but it also silently cuts off what they paste, and they may never notice that the end is missing. If there's a limit, we recommend saying what it is in the hint, letting people go over it, and saying by how much in an error message, so they can decide what to cut.

Every state

Disabled

A disabled input can't be focused, edited or submitted. disabled sets the native attribute, so the input leaves the tab order, screen readers announce it as dimmed or unavailable when they read through the page, and its form leaves its value out when it's sent. That last part is easy to miss: if people need to see a value, and the form needs to send it, the input should be read-only instead.

We recommend disabling as little as you can, since a disabled input can't explain why it's disabled, and keyboard users can't even reach it to find out. Explain why next to it, or show the value as text instead.

Never disable an input while it has focus, either, like every input in a form while it's being sent. Disabling a focused input takes its focus away, and keyboard and screen reader users lose their place. Make the inputs read-only while the form is sent instead, which keeps the focus where it is.

Read-only

A read-only input shows a value that people can't change, like an account ID. It stays in the tab order, so people can focus it, select the value and copy it, screen readers announce it as read-only, and its form still sends it. readonly is a native attribute, so it lands on the <input> like any other. We recommend a dashed border, which tells it apart from an input you can type in, while the value keeps its full contrast.

Invalid

An invalid input has a value that needs fixing, and an error message that says how. The Input shows the state, and the page decides when to show it, since only the page knows what makes a value right.

import { useRef, useState, type FormEvent } from "react";
import { flushSync } from "react-dom";

export function Subscribe() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");
  const [status, setStatus] = useState("");
  const input = useRef<HTMLInputElement>(null);

  // Only checks what people have typed, so tabbing past the input doesn't flag it.
  function leave() {
    if (email) setError(validateEmail(email));
  }

  function update(value: string) {
    setEmail(value);
    // Once an error shows, check again as people type, so it clears as soon as it's fixed.
    if (error) setError(validateEmail(value));
  }

  async function subscribe(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const message = validateEmail(email);
    if (message) {
      // Focus once the error is in the page, so screen readers read it with the input.
      flushSync(() => {
        setError(message);
        setStatus("Couldn't subscribe. Check your email address.");
      });
      input.current?.focus();
      return;
    }
    await api.subscribe(email);
    setStatus("You're subscribed");
  }

  return (
    <form noValidate onSubmit={subscribe}>
      <label htmlFor="email">Email address</label>
      <Input
        ref={input}
        id="email"
        type="email"
        autoComplete="email"
        required
        value={email}
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? "email-error" : undefined}
        onChange={(event) => update(event.target.value)}
        onBlur={leave}
      />
      {error && <p id="email-error">{error}</p>}
      <Button type="submit" color="primary">Subscribe</Button>
      <p role="status">{status}</p>
    </form>
  );
}

validateEmail stands in for your own check, which returns an error message, or an empty string when the address looks right.

  • Set aria-invalid="true" on the input while its error message shows, and point aria-describedby at the message. Screen readers then announce the input as invalid, and read the message after its name (WCAG 3.3.1).
  • Say what's wrong and how to fix it, as in "Enter an email address in the correct format, like name@example.com", rather than "Invalid input" (WCAG 3.3.3). A red border alone can't say any of that, and it means nothing to people who can't tell red apart (WCAG 1.4.1).
  • Check a value when people leave the input, or when they submit, and not while they're still typing it. An error that shows up after the first letter only tells people what they already know. Once an error shows, though, check again as they type, and clear it as soon as the value is right.
  • When a submit fails, move focus to the first invalid input, so keyboard and screen reader users land right on it and hear its error. Do it once the error is in the page, which in most frameworks means waiting for the next render, as above. If the input already has focus, as it does when people press Enter in it, moving focus doesn't announce anything, which is why the example also says what happened in a status message. With several errors, a summary at the top of the form, with a link to each input, works even better.
  • Put novalidate on the form, so that the browser's own error bubbles, which look different in every browser and vanish after a few seconds, don't compete with yours. Keep required on the input, though, which tells screen readers that it's required.

Passwords

A password input hides what people type, which makes typos hard to spot. Let them check what they've typed with a Show password button in actions. It's a toggle button that switches the input's type to text and back, so it keeps its label and says whether it's on with aria-pressed.

const [shown, setShown] = useState(false);

<label htmlFor="password">Password</label>
<Input
  id="password"
  type={shown ? "text" : "password"}
  autoComplete="current-password"
  actions={
    <Button
      label="Show password"
      variant="ghost"
      size="xs"
      iconOnly
      aria-controls="password"
      aria-pressed={shown}
      onClick={() => setShown(!shown)}
      leading={shown ? <EyeOffIcon /> : <EyeIcon />}
    />
  }
/>
  • Use autocomplete="current-password" to sign in and "new-password" to sign up, so password managers fill in the right one, or offer to make up a strong one.
  • The toggle is a Button, so its type is "button", and it never sends the form by accident (see the Button's Don't submit forms by accident).
  • Edge draws a Show password button of its own once people start typing, so the Input hides it when it has actions, to make sure there's only ever one.

Adaptive

Pointer, touch and pen

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the border looking hovered.
  • On touch screens, the input reaches past the field to a hit area at least 44px tall, without the field looking any bigger. It does it with a transparent border, which the browser's autofill colour doesn't paint.
  • On touch screens, the text is at least 16px, whatever the size. Safari on iOS zooms in on an input with smaller text as soon as it gets focus, and people then have to zoom back out to see the rest of the form.
  • Pressing an icon, a prefix or the padding focuses the input, as pressing the input would. It happens on mousedown, so an input that already has focus keeps it, without a blur that would run its validation.

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the border is drawn in the text colour at full strength, rather than at 55%. The value and the placeholder already pass 4.5:1, so they stay as they are.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds and shadows. The Input's border stays, since the system draws it in a colour of its own, focus is drawn with outline, and a disabled input is marked with GrayText. A read-only input's border stays dashed. The invalid border's second pixel is a shadow, which forced colours drop, so in these themes it's the error message, in text, that tells people something's wrong, which is one more reason to always show one.

Colour scheme

The tokens use light-dark(), which follows color-scheme, so the Input adapts to your light and dark themes on its own. Setting color-scheme on your themes also lets the browser match the parts of the input it draws itself, like the autofill colour in Chrome.

Autofill

Browsers tint the inputs they fill in, so people can see what was filled in for them. We recommend leaving the tint as it is. It's useful, and the tricks that hide it, like a huge inset box-shadow, break as browsers change. The Input rounds the <input>'s corners so the tint follows the field's, and keeps it inside the field on touch screens.

Text size and spacing

Heights are minimums in rem, so the field grows with its text instead of clipping it, at 200% text size (WCAG 1.4.4) and with wider line, letter and word spacing (WCAG 1.4.12). A value that's longer than the input scrolls inside it, as in any single-line input, so for text that runs long, like a message, a Textarea is a better fit.

Languages and direction

The Input uses logical properties and leading and trailing slots rather than left and right, so it mirrors itself in right-to-left languages, prefix, suffix and all. The value needs a little help, though:

<div dir="rtl" lang="ar">
  <label htmlFor="name">الاسم</label>
  <Input id="name" dir="auto" autoComplete="name" />

  <label htmlFor="email">البريد الإلكتروني</label>
  <Input id="email" type="email" dir="ltr" autoComplete="email" leading={<MailIcon />} />
</div>
  • Some values always run left to right, like email addresses, phone numbers and web addresses. Give their inputs dir="ltr", which lands on the <input>, so the value reads the right way round while the field around it, icon included, stays right to left.
  • For free text, which people could type in either direction, like a name, dir="auto" lets the input follow whatever they type.
  • Remember to translate labels, hints and error messages along with the rest of the page, and to mark any that are in another language with lang, so that screen readers pronounce them correctly (WCAG 3.1.2).

UX Checklist

RuleLevelScopeRequirementBasisCheck
input/native-elementMustComponentIt's a native <input>, and never a contenteditable elementOpen Components, beyond WCAG 2.1.1, 4.1.2 (A)Unit test
input/visible-labelMustUsageEvery input has a visible <label>, linked with for, rather than a placeholder alone, unless it's a search next to its Search button, named with aria-labelWCAG 1.3.1, 3.3.2, 4.1.2 (A)axe label
input/label-in-nameMustUsageAn aria-label or aria-labelledby includes the visible label, ideally at the startWCAG 2.5.3 (A)axe label-content-name-mismatch
input/describedShouldUsageHints, like the format it expects, are linked with aria-describedby, and a placeholder only ever shows an exampleWCAG 1.3.1 (A), Open ComponentsScreen reader
input/decorative-slotsMustComponentleading and trailing are hidden from assistive technologies with aria-hidden, while actions stay reachableWCAG 1.1.1 (A)Unit test
input/units-in-labelMustUsageA prefix or a suffix, like a currency or a unit, is in the label or the hint as wellWCAG 1.3.1 (A)Review
input/input-purposeMustUsageAn input that asks about the person filling it in has the matching autocomplete, like email or telWCAG 1.3.5 (AA)axe autocomplete-valid, review
input/fitting-typeShouldUsageThe type or inputmode fits the value, so phones show the right keyboard, digits that aren't amounts use inputmode="numeric" rather than type="number", and values that aren't words turn off spellcheckOpen ComponentsReview
input/keyboardMustComponentTab reaches the input and then its actions, and the browser's text editing keys work as they do in any text fieldWCAG 2.1.1 (A)Unit test
input/allow-pasteMustBothPasting, autofill and password managers are never blockedWCAG 3.3.8 (AA)Unit test, review
input/enter-submitsShouldBothEnter submits the input's form, which has a submit buttonHTMLUnit test
input/no-truncationShouldUsageA length limit is stated in the hint and checked with an error message, rather than enforced with maxlengthOpen ComponentsReview
input/error-messageMustUsageAn invalid input has an error message, in text and linked with aria-describedby, that says what's wrong and how to fix itWCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)Screen reader
input/invalid-stateMustBothAn input sets aria-invalid="true" while its error message shows, and only looks invalid when it doesWCAG 4.1.2 (A), Open ComponentsUnit test
input/validate-lateShouldUsageErrors show when people leave the input or submit, never while they're first typing, and clear as soon as the value is fixedOpen ComponentsReview
input/focus-first-errorShouldUsageWhen a submit fails, focus moves to the first invalid input, or to a summary of the errors, once the error messages are in the pageOpen ComponentsKeyboard, screen reader
input/disabled-inertMustComponentA disabled input can't be focused, edited or submittedHTMLUnit test
input/read-only-valuesShouldUsageA value people need to see but can't change is read-only rather than disabled, so they can focus it and copy it, and its form sends itOpen ComponentsReview
input/keep-focusMustUsageAn input is never disabled while it has focus, so while its form is sent, it's read-only insteadOpen ComponentsKeyboard
input/action-buttonsMustBothButtons in actions are named Buttons that Tab reaches after the input, and one that goes away moves focus back to the inputWCAG 2.1.1, 2.4.3, 4.1.2 (A)Unit test, keyboard
input/show-passwordShouldUsageA password input has a Show password toggle in actions, which sets aria-pressed and keeps its labelOpen ComponentsReview
input/focus-not-obscuredMustUsageSticky headers and footers never cover a focused input, for example thanks to scroll-paddingWCAG 2.4.11 (AA), 2.4.12 (AAA)Keyboard
input/mirrors-in-rtlShouldBothIt uses logical properties, and values that always run left to right, like email addresses, get dir="ltr" in right-to-left layoutsOpen ComponentsReview

FAQ

Input or something else?

An Input takes a single line of text. For anything else, another component will do a better job:

When people need to…Use
Type a single line of text, like a name or an email addressInput
Type a single line of digits, like a security codeInput, with inputmode="numeric"
Write something longer, like a comment or a messageTextarea
Pick one option out of a few, or out of manyRadio Group, or Select
Pick any number of optionsCheckbox Group
Type to filter a list of suggestionsCombobox
Pick a dateDate Picker
Turn something on or offCheckbox, or Switch

The Textarea, the Checkbox Group, the Checkbox and the Switch take the same size names as the Input, and point to their hints and error messages by ID, as it does. The Radio Group, the Select, the Combobox and the Date Picker are on our Roadmap, and they'll share the Input's sizes, states and rules for labels and errors.

Copyright © 2026