User Experience (UX)
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 withforand the input'sid(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'sid. - 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-labeloraria-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
idfor 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
| Key | Input |
|---|---|
Tab, Shift+Tab | Moves focus to and from the input, and on to its actions |
Arrows, Home and End | Move the caret, as in any other text field |
Enter | Submits 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… | Use | Phones show | autocomplete, for example |
|---|---|---|---|
| A name, or any other text | type="text", the default | The standard keyboard | name, given-name, organization |
| An email address | type="email" | A keyboard with @ and . | email |
| A password | type="password" | The standard keyboard, hiding the value | current-password, new-password |
| A search | type="search" | A keyboard with a search key | None |
| A phone number | type="tel" | A phone keypad | tel |
| A web address | type="url" | A keyboard with / and . | url |
| A code, or a card number | type="text" with inputmode="numeric" | A number keypad | one-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
autocompletevalue, likeemail,telorpostal-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, andautocapitalizeandautocorrectwhere thetypedoesn't already, so phones don't capitalise the first letter or "correct" a username into a word. - If you'd like the
Enterkey on phones to say what it does, setenterkeyhint, as inenterkeyhint="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 pointaria-describedbyat 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
Enterin 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
novalidateon 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. Keeprequiredon 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
typeis"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 ablurthat 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
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
input/native-element | Must | Component | It's a native <input>, and never a contenteditable element | Open Components, beyond WCAG 2.1.1, 4.1.2 (A) | Unit test |
input/visible-label | Must | Usage | Every 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-label | WCAG 1.3.1, 3.3.2, 4.1.2 (A) | axe label |
input/label-in-name | Must | Usage | An aria-label or aria-labelledby includes the visible label, ideally at the start | WCAG 2.5.3 (A) | axe label-content-name-mismatch |
input/described | Should | Usage | Hints, like the format it expects, are linked with aria-describedby, and a placeholder only ever shows an example | WCAG 1.3.1 (A), Open Components | Screen reader |
input/decorative-slots | Must | Component | leading and trailing are hidden from assistive technologies with aria-hidden, while actions stay reachable | WCAG 1.1.1 (A) | Unit test |
input/units-in-label | Must | Usage | A prefix or a suffix, like a currency or a unit, is in the label or the hint as well | WCAG 1.3.1 (A) | Review |
input/input-purpose | Must | Usage | An input that asks about the person filling it in has the matching autocomplete, like email or tel | WCAG 1.3.5 (AA) | axe autocomplete-valid, review |
input/fitting-type | Should | Usage | The 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 spellcheck | Open Components | Review |
input/keyboard | Must | Component | Tab reaches the input and then its actions, and the browser's text editing keys work as they do in any text field | WCAG 2.1.1 (A) | Unit test |
input/allow-paste | Must | Both | Pasting, autofill and password managers are never blocked | WCAG 3.3.8 (AA) | Unit test, review |
input/enter-submits | Should | Both | Enter submits the input's form, which has a submit button | HTML | Unit test |
input/no-truncation | Should | Usage | A length limit is stated in the hint and checked with an error message, rather than enforced with maxlength | Open Components | Review |
input/error-message | Must | Usage | An invalid input has an error message, in text and linked with aria-describedby, that says what's wrong and how to fix it | WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA) | Screen reader |
input/invalid-state | Must | Both | An input sets aria-invalid="true" while its error message shows, and only looks invalid when it does | WCAG 4.1.2 (A), Open Components | Unit test |
input/validate-late | Should | Usage | Errors show when people leave the input or submit, never while they're first typing, and clear as soon as the value is fixed | Open Components | Review |
input/focus-first-error | Should | Usage | When a submit fails, focus moves to the first invalid input, or to a summary of the errors, once the error messages are in the page | Open Components | Keyboard, screen reader |
input/disabled-inert | Must | Component | A disabled input can't be focused, edited or submitted | HTML | Unit test |
input/read-only-values | Should | Usage | A 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 it | Open Components | Review |
input/keep-focus | Must | Usage | An input is never disabled while it has focus, so while its form is sent, it's read-only instead | Open Components | Keyboard |
input/action-buttons | Must | Both | Buttons in actions are named Buttons that Tab reaches after the input, and one that goes away moves focus back to the input | WCAG 2.1.1, 2.4.3, 4.1.2 (A) | Unit test, keyboard |
input/show-password | Should | Usage | A password input has a Show password toggle in actions, which sets aria-pressed and keeps its label | Open Components | Review |
input/focus-not-obscured | Must | Usage | Sticky headers and footers never cover a focused input, for example thanks to scroll-padding | WCAG 2.4.11 (AA), 2.4.12 (AAA) | Keyboard |
input/mirrors-in-rtl | Should | Both | It uses logical properties, and values that always run left to right, like email addresses, get dir="ltr" in right-to-left layouts | Open Components | Review |
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 address | Input |
| Type a single line of digits, like a security code | Input, with inputmode="numeric" |
| Write something longer, like a comment or a message | Textarea |
| Pick one option out of a few, or out of many | Radio Group, or Select |
| Pick any number of options | Checkbox Group |
| Type to filter a list of suggestions | Combobox |
| Pick a date | Date Picker |
| Turn something on or off | Checkbox, 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.
required on the inputs that are, which tells screen readers.:invalid also matches before people have typed anything. Keep the attributes that describe the value, like required and type="email", put novalidate on the form, and show your own error messages, as under Invalid.