Input
An input is a single-line text field. It has hover, focus, disabled and invalid states.
Enter a valid email address.
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function InputDemo() {
return (
<div className="flex w-full max-w-xs flex-col gap-4">
<Input aria-label="Default" placeholder="Default" />
<div className="flex flex-col gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Njoh Simplice" />
</div>
<Input aria-label="Disabled" placeholder="Disabled" disabled />
<div className="flex flex-col gap-2">
<Input
aria-label="Email"
aria-invalid
aria-describedby="email-error"
defaultValue="not-an-email"
/>
<p id="email-error" className="text-sm text-danger">
Enter a valid email address.
</p>
</div>
</div>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { Input as BaseInput } from "@base-ui/react/input";
import { cx } from "@/lib/cx";
import { controlStyles } from "@/lib/styles";
export function Input({
className,
...props
}: Omit<ComponentProps<typeof BaseInput>, "className"> & { className?: string }) {
return <BaseInput className={cx("h-9", controlStyles, className)} {...props} />;
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
type | string | text | The kind of text: text, email, password, and so on. |
placeholder | string | — | A hint shown while the field is empty. It is not a name for the field. |
disabled | boolean | false | Turns the field off. It can't be focused or edited. |
aria-invalid | boolean | — | Marks the value as wrong, and turns the border to the danger color. |
className | string | — | Extra classes, added after the input's own. |
Every other prop goes to the Base UI Input underneath, so it behaves like a native input.
The thinking
A text field should look like one and feel the same everywhere, so it has no variants, just states.
- Built on Base UI: It is Base UI's Input, which also reports its state when you place it inside a Base UI Field.
- Invalid is a state: Set aria-invalid and the border turns to the danger color. Base UI's own invalid flag does the same.
- The label is separate: Pair it with a Label using htmlFor. The placeholder is a hint, not a name.
Interface design
- Size: 36px tall, to match Button, with 12px of side padding, 16px text and the md radius, 6px.
- Outline: A 1px border in the secondary text color, 4.2:1 against the page, on a see-through fill.
- Invalid: A danger-colored border. Put the message under the field in danger-colored text, as the demo does.
- Disabled: A surface fill, a hairline border and quiet text.
Interaction
- Hover: The border brightens, but only on an input that is valid and enabled.
- Focus: A 2px outline 2px away from the field, the same as every control.
- Invalid: Keeps its danger border on hover and focus, so the problem doesn't disappear when you click in.
- Disabled: It can't be focused or edited, and shows a not-allowed cursor.
Empathy
- Name every field: Use a Label with
htmlFor, or anaria-label. The bare fields in the demo have one. - Say what is wrong: A red border alone isn't enough. Write the message next to the field and tie it in with
aria-describedby, as the invalid example does. - A placeholder is not a label: It vanishes as soon as someone types, and its color is 4.2:1 against the page, just under the 4.5:1 that AA asks for.
- Target size: 36px tall clears the 24px minimum of WCAG 2.2 AA.