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

PropTypeDefaultWhat it does
typestringtextThe kind of text: text, email, password, and so on.
placeholderstring—A hint shown while the field is empty. It is not a name for the field.
disabledbooleanfalseTurns the field off. It can't be focused or edited.
aria-invalidboolean—Marks the value as wrong, and turns the border to the danger color.
classNamestring—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 an aria-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.