# Input

> Collect a short, single-line value such as a name, email address, or search term.

Source: https://www.honestui.com/docs/components/input

```tsx
import { Input } from "@/components/honest-ui/ui/input"

export function InputDemo() {
  return (
    <Input className="w-full max-w-64" placeholder="Enter text" aria-label="Enter text" />
  )
}

```

## Overview [#overview]

Use Input for short free-form text, numbers, email addresses, URLs, search terms, and file selection. Input is a low-level control: in forms, wrap it with Field so every input gets a label, description, error, and disabled state in a consistent layout.

## Anatomy [#anatomy]

An input has a value, an optional placeholder, a type, a size, and a state. The visible label names what belongs in the field; the placeholder only shows an example and disappears at first keystroke. A placeholder is never a substitute for a label.

## Sizes and types [#sizes-and-types]

Sizes are `sm` (24 px tall), `default` (32 px), and `lg` (40 px). Use `default` inside forms, `sm` in dense toolbars and table rows, and `lg` for hero search or single-field pages.

Choose the native `type` that matches the data: `email`, `tel`, `url`, `search`, `number`, `date`, and `file` each bring the right keyboard on touch devices and built-in validation. For values that look numeric but are identifiers rather than quantities, such as card numbers, verification codes, or phone numbers, use `type="text"` with `inputMode="numeric"` or the matching `type`. See [Don't do this](#dont-do-this) for why.

The `borderless` variant removes the surrounding border and shows a focus ring instead, which suits inputs embedded in toolbars or command bars.

## States [#states]

**Disabled** inputs are skipped by keyboard focus, excluded from form submission, dimmed, and show a not-allowed cursor. Use them when the field never applies in the current state.

**Read-only** inputs keep their value selectable and copyable and still submit with the form, but cannot be edited. Prefer read-only over disabled when you are showing data that exists and matters, such as a generated slug or a computed price.

**Invalid.** Setting `aria-invalid="true"` switches the border to the danger color, so validation state is exposed to assistive technology as well as visually. Pair it with a FieldError that states what to fix.

## Accessibility [#accessibility]

Every input needs a label connected through `htmlFor`/`id` or a Field wrapper. Focus moves into the input with <kbd>Tab</kbd>, and standard text-editing keys work as they do natively. The focus indicator is a border and background change on the wrapper; the borderless variant shows a visible ring instead.

The default height of 32 px clears the WCAG 2.2 minimum target size of 24 px, but leave extra spacing around inputs used as primary touch targets on mobile.

Placeholder text uses the muted foreground token, which adapts to dark mode automatically. Test browser autofill in dark themes: some browsers paint autofilled fields with their own light backgrounds unless overridden.

In right-to-left layouts the input mirrors correctly because its padding uses logical properties. Mixed-direction content such as a URL typed into an Arabic page follows the browser's own bidirectional rules.

Long values scroll horizontally inside the control; they never wrap or grow the field. If people need to see the whole value at once, use Textarea.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;input&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/input.tsx

```tsx
"use client"

import { Input as InputPrimitive } from "@base-ui-components/react/input"

import { cn } from "@/lib/utils"

function Input({
  className,
  size = "default",
  variant = "default",
  ...props
}: Omit<InputPrimitive.Props, "size"> & {
  size?: "sm" | "default" | "lg" | number
  variant?: "default" | "borderless"
}) {
  return (
    <span
      data-slot="input-control"
      data-disabled={props.disabled ? "" : undefined}
      data-variant={variant}
      className={cn(
        "relative flex min-h-[var(--hui-space-9)] w-full items-center overflow-hidden rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-tertiary)] bg-[var(--hui-color-background-base-primary)] [transition:var(--hui-transition-interactive)] focus-within:border-[var(--hui-color-border-accent-emphasis)] focus-within:bg-[var(--hui-color-background-base-primary)] focus-within:outline-none has-[[data-slot=input][data-active=true]]:border-[var(--hui-color-border-accent-emphasis)] has-[[data-slot=input][data-invalid]]:border-[var(--hui-color-border-danger-emphasis)] has-[[data-slot=input][data-invalid]]:focus-within:border-[var(--hui-color-border-danger-emphasis-hover)] has-[[data-slot=input][aria-invalid=true]]:border-[var(--hui-color-border-danger-emphasis)] has-[[data-slot=input][aria-invalid=true]]:focus-within:border-[var(--hui-color-border-danger-emphasis-hover)] has-[[data-slot=input][data-disabled]]:cursor-not-allowed has-[[data-slot=input][data-disabled]]:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50",
        size === "sm" && "min-h-[var(--hui-space-7)]",
        size === "lg" && "min-h-[var(--hui-space-10)]",
        variant === "borderless" &&
          "border-transparent focus-within:border-transparent has-[[data-slot=input]:focus-visible]:shadow-[var(--hui-focus-ring-shadow)]",
        className
      )}
    >
      <InputPrimitive
        data-slot="input"
        className={cn(
          "m-0 box-border h-[var(--hui-space-9)] w-full min-w-0 rounded-none border-0 bg-transparent pe-[var(--hui-space-2)] ps-[var(--hui-space-3)] text-[var(--hui-color-foreground-base-primary)] outline-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-t2)] placeholder:text-[var(--hui-color-foreground-base-tertiary)] placeholder:[font-size:var(--hui-font-size-small)] placeholder:[font-weight:var(--hui-font-weight-regular)] placeholder:[letter-spacing:var(--hui-letter-spacing-small)] placeholder:[line-height:var(--hui-line-height-t2)] focus:border-[var(--hui-color-border-accent-emphasis)] data-disabled:cursor-not-allowed data-disabled:text-[var(--hui-color-foreground-base-tertiary)]",
          size === "sm" &&
            "h-[var(--hui-space-7)] [line-height:var(--hui-line-height-large)]",
          size === "lg" && "h-[var(--hui-space-10)]",
          props.type === "search" &&
            "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
          props.type === "file" &&
            "text-muted-foreground file:me-3 file:bg-transparent file:text-sm file:font-medium file:text-foreground"
        )}
        size={typeof size === "number" ? size : undefined}
        {...props}
      />
    </span>
  )
}

export { Input }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { Input } from "@/components/ui/input";
```

```tsx
<Input name="email" type="email" />
```

For accessible labeling and validation, connect the input to a label with `Label` and `htmlFor`/`id`, or use Field and FieldControl. See the [Field examples](/docs/components/field).

## Don't do this [#dont-do-this]

### Placeholder as label [#placeholder-as-label]

```tsx
// Bad
<Input placeholder="Email address" name="email" type="email" />
```

```tsx
// Good
<Label htmlFor={id}>Email address</Label>
<Input id={id} placeholder="you@example.com" name="email" type="email" />
```

A placeholder disappears the moment someone starts typing, so anyone reviewing or correcting the form can no longer see which field holds what. Placeholders also commonly fail contrast requirements and are not reliably announced by screen readers as a field's name. Keep the label permanent; let the placeholder show format, like `you@example.com`.

### Number inputs for identifiers [#number-inputs-for-identifiers]

```tsx
// Bad
<Input name="card" type="number" placeholder="Card number" />
```

```tsx
// Good
<Label htmlFor={cardId}>Card number</Label>
<Input
  id={cardId}
  name="card"
  type="text"
  inputMode="numeric"
  autoComplete="cc-number"
  maxLength={19}
/>
```

A card number is not a quantity. `type="number"` attaches increment arrows that corrupt the value with a stray click, silently rejects leading zeros, and accepts exponent notation. Identifiers should be text fields with a numeric touch keyboard (`inputMode`) and the matching `autoComplete` token.

## Examples [#examples]

### Sizes [#sizes]

```tsx
import { useId } from "react";

import { Input } from "@/components/honest-ui/ui/input";
import { Label } from "@/components/honest-ui/ui/label";

export function InputSizes() {
  const smId = useId();
  const defaultId = useId();
  const lgId = useId();

  return (
    <div className="grid w-full max-w-64 gap-4">
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={smId}>Small</Label>
        <Input id={smId} size="sm" placeholder="Enter text" />
      </div>
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={defaultId}>Default</Label>
        <Input id={defaultId} placeholder="Enter text" />
      </div>
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={lgId}>Large</Label>
        <Input id={lgId} size="lg" placeholder="Enter text" />
      </div>
    </div>
  );
}

```

### Disabled and read-only [#disabled-and-read-only]

Read-only keeps the value selectable and submitted; disabled removes the field from interaction entirely.

```tsx
import { useId } from "react";

import { Input } from "@/components/honest-ui/ui/input";
import { Label } from "@/components/honest-ui/ui/label";

export function InputStates() {
  const readOnlyId = useId();
  const disabledId = useId();

  return (
    <div className="grid w-full max-w-64 gap-4">
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={readOnlyId}>Workspace slug (read-only)</Label>
        <Input id={readOnlyId} defaultValue="aurora-production" readOnly />
      </div>
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={disabledId}>Seat count (unavailable)</Label>
        <Input id={disabledId} defaultValue="24" disabled />
      </div>
    </div>
  );
}

```

### File selection [#file-selection]

```tsx
import { Input } from "@/components/honest-ui/ui/input"

export function InputFile() {
  return <Input className="w-full max-w-64" type="file" aria-label="File" />
}

```

### Long values [#long-values]

Values longer than the field scroll horizontally instead of wrapping.

```tsx
import { useId } from "react";

import { Input } from "@/components/honest-ui/ui/input";
import { Label } from "@/components/honest-ui/ui/label";

const LONG_URL =
  "https://example.com/knowledge-base/articles/understanding-prorated-billing-for-annual-plan-upgrades";

export function InputLongText() {
  const id = useId();

  return (
    <div className="flex w-full max-w-sm flex-col items-start gap-2">
      <Label htmlFor={id}>Canonical URL</Label>
      <Input id={id} type="url" defaultValue={LONG_URL} />
    </div>
  );
}

```

### Right-to-left languages [#right-to-left-languages]

```tsx
import { useId } from "react";

import { Input } from "@/components/honest-ui/ui/input";
import { Label } from "@/components/honest-ui/ui/label";

export function InputRtl() {
  const id = useId();

  return (
    <div dir="rtl" lang="ar" className="w-full max-w-64">
      <div className="flex flex-col items-start gap-2">
        <Label htmlFor={id}>ابحث في المساعدة</Label>
        <Input id={id} type="search" placeholder="اكتب كلمة للبحث" />
      </div>
    </div>
  );
}

```

### With attached button [#with-attached-button]

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import { Input } from "@/components/honest-ui/ui/input"

export function InputWithButton() {
  return (
    <div className="w-full max-w-64 flex gap-2">
      <Input
        type="email"
        placeholder="you@example.com"
        aria-label="Email"
      />
      <Button variant="secondary">Send</Button>
    </div>
  )
}

```

### Form integration [#form-integration]

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Field,
  FieldControl,
  FieldError,
  FieldLabel,
} from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";

export function FormDemo() {
  const [savedEmail, setSavedEmail] = React.useState("");

  const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    setSavedEmail(String(formData.get("email") ?? ""));
  };

  return (
    <Form onSubmit={onSubmit} className="grid w-full max-w-64 gap-4">
      <Field>
        <FieldLabel>Email address</FieldLabel>
        <FieldControl
          name="email"
          type="email"
          placeholder="you@example.com"
          required
        />
        <FieldError>
          Enter an email address in the format name@example.com.
        </FieldError>
      </Field>
      <Button type="submit">Save email address</Button>
      <p
        aria-live="polite"
        className="min-h-[var(--hui-space-5)] text-[length:var(--hui-font-size-mini)] text-[var(--hui-color-foreground-success-primary)]"
      >
        {savedEmail ? `Submitted email: ${savedEmail}.` : null}
      </p>
    </Form>
  );
}

```

## API reference [#api-reference]

`Input` accepts all Base UI Input props plus Honest UI additions:

| Prop      | Values                                                 | Default     |
| --------- | ------------------------------------------------------ | ----------- |
| `size`    | `"sm"`, `"default"`, `"lg"`, or a number of characters | `"default"` |
| `variant` | `"default"`, `"borderless"`                            | `"default"` |

Native attributes such as `type`, `name`, `required`, `disabled`, `readOnly`, `autoComplete`, `inputMode`, `minLength`, and `pattern` pass straight through to the underlying `<input>`. Validation state is driven by `aria-invalid`. Use controlled state only when the application needs it; uncontrolled inputs work well with `FormData`.

See the [Base UI Input API](https://base-ui.com/react/components/input#api-reference).
