Input

Developer Experience (DX)

How developers use the Input, with one consistent, typed API that works with any form and other components, whether its value is bound or not.

Next, let's look at how developers use the Input. It has one API, with the same names as every other component, it's typed end to end, and it works with your forms however you build them.

import { useState } from "react";
import { Input } from "@/components/Input";

export function Newsletter() {
  const [email, setEmail] = useState("");

  return (
    <>
      <label htmlFor="email">Email address</label>
      <Input
        id="email"
        type="email"
        autoComplete="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
      />
    </>
  );
}

Consistent

Once you've learned the Button's API, you already know most of the Input's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropxs, sm, md, lg or xl, with the same heights everywhere
disabledPropThe component can't be used
leading, trailingSlotsContent before and after the main content
actionsSlotButtons that act on the component's value, after everything else
valuePropThe value, which you bind the way your framework binds a form control's

We recommend size="md" and type="text" as the defaults. The Input also leaves a few props out, on purpose:

  • There's no variant. Buttons need ranking, so there's one primary action per view, but the inputs in a form are all equally important. A variant without a border, like ghost, would also leave an empty input invisible (WCAG 1.4.11).
  • There's no color either. An input has no intent of its own, and its one coloured state, invalid, comes from aria-invalid, so its colour and its state can't disagree.
  • There's no label, hint or error. Each of them is an element of its own, linked by ID, which the Field on our Roadmap will render, so the Input stays one control that fits any layout.
  • There's no loading. If a value is being checked, like whether a username is free, put a Spinner in trailing, and say what's happening in a status message.

Type-safe

Props, events and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like type="number", which isn't one of the Input's types.

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"How big the input is
type"text" | "email" | "password" | "search" | "tel" | "url""text"The kind of text it takes, which picks the keyboard on phones
disabledbooleanfalseBlocks focus, editing and submission
valuestringThe value, when you bind it. Without it, the input keeps its own
EventPayloadDescription
inputEventFires as people type
changeEventFires when people commit a change, like when they leave the input

Both come straight from the <input>, like every other native event, such as focus and blur, and a binding to value listens to them for you.

SlotDescription
leadingAn icon or a prefix before the value, hidden from assistive technologies
trailingAn icon or a suffix after the value, hidden from assistive technologies
actionsButtons that act on the value, which Tab reaches after the input

We also export InputProps, which comes in handy for wrappers and data-driven forms:

import type { InputProps } from "@/components/Input";

const fields: (InputProps & { name: string; label: string })[] = [
  { name: "email", label: "Email address", type: "email" },
  { name: "phone", label: "Phone number", type: "tel" },
];

Composable

The Input renders two elements, so it splits what you pass between them. Classes and styles go on the root, which draws the field, so a class can set its variables and its width. Everything else lands on the <input>, including id, name, autocomplete, inputmode, placeholder, required, readonly, ARIA attributes and event listeners. A ref to the Input reaches the <input> too, since that's what form libraries and focus() need.

Forms

The <input> is a real form control, so the Input works with your forms however you build them. You can bind its value, as above, or leave it to the browser and read it from the form when it's sent, by its name. That also means the form works before your JavaScript loads.

import type { FormEvent } from "react";

export function SignUp() {
  function signUp(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    api.signUp(data.get("username"), data.get("password"));
  }

  return (
    <form onSubmit={signUp}>
      <label htmlFor="username">Username</label>
      <Input
        id="username"
        name="username"
        autoComplete="username"
        autoCapitalize="none"
        autoCorrect="off"
        spellCheck={false}
      />
      <label htmlFor="password">Password</label>
      <Input id="password" name="password" type="password" autoComplete="new-password" />
      <Button type="submit" color="primary">Create account</Button>
    </form>
  );
}

Form libraries work the same way. They register the <input> itself, through its ref or its attributes and listeners, which is why the Input puts them all on the <input>.

With other components

  • The Field, on our Roadmap, will render the label, hint and error message around an Input, give it an id, and set its aria-describedby and aria-invalid for you.
  • A Button next to an input, like Search, takes the same size, so the two line up. A Button in actions is ghost and icon-only, two sizes smaller, and it's still a Button, with its name, its focus ring and its type="button".
  • A Spinner in trailing shows that a value is being checked, while a status message says what's happening, since the Spinner stays hidden from screen readers.
  • Components that take typed text, like a Combobox or a Date Picker, will build on the Input, with the same sizes, slots and DOM.

Controllable

The Input doesn't keep any state you can't reach. The value is yours to bind, or to leave to the browser, and every other state is a prop or an attribute that you control:

StateHow you control it
Valuevalue, bound to your state, or left to the browser
Disableddisabled
Read-onlyreadonly, which lands on the <input>
Invalidaria-invalid, set while its error message shows (see Invalid)
FocusA ref to its <input>, and focus()

DX Checklist

RuleLevelScopeRequirementBasisCheck
input/shared-vocabularyMustComponentIt uses the shared names: size, disabled, leading, trailing and actionsOpen ComponentsReview
input/typedMustComponentProps, events and slots are typed, with unions rather than string, and type only takes the text typesOpen ComponentsType check
input/control-attributesMustComponentAttributes, listeners and refs land on the <input>, while classes and styles land on the rootOpen ComponentsUnit test
input/form-controlMustComponentIts form sends its value under its name, whether the value is bound or notHTMLUnit test
input/controllableShouldComponentIts value can be bound or left to the browser, and every other state is a prop or an attributeOpen ComponentsUnit test

FAQ

Copyright © 2026