# Shadcn Input

> Free shadcn/ui input component for React with label, search, password, file and validation examples. Theme-aware, accessible, copy or install in one command.

Source: https://designrevision.com/components/input

The **Shadcn Input** is the foundation of every form in your interface — the canonical <a href="https://ui.shadcn.com/docs/components/input" rel="nofollow">shadcn/ui</a> input, a native `<input>` element themed entirely with CSS variables. It stays deliberately minimal so it composes with everything: pair it with a [Label](/components/label), wrap it with an icon or a [Button](/components/button), or drop in a [Kbd](/components/kbd) shortcut hint. The examples below cover the patterns you reach for most.

## When to use

Use an input for **short, free-form text** — an email, a name, a search query, a URL. For a single multi-line value use a textarea; for choosing from a fixed set use a select or radio group; for on/off use a switch or checkbox. Match the `type` to the data (`email`, `password`, `search`, `file`, `url`, `tel`) so browsers offer the right keyboard, autofill, and validation.

## Anatomy

A complete field is three parts: a **label**, the **input**, and optional **helper or error text**. Bind the label to the input with `htmlFor`/`id` so clicking the label focuses the field and screen readers announce them together. Helper text sits below in muted foreground; an error message replaces it and is wired up with `aria-describedby`. Icons, add-ons, counters, and buttons layer on top of this base without changing it.

## Validation and errors

Error state is driven by the standard **`aria-invalid`** attribute, not a custom prop — set it and the input switches on the destructive border and focus ring automatically. This keeps the component compatible with native HTML validation and form libraries like React Hook Form, which toggle `aria-invalid` for you. Always pair the invalid field with a visible message linked by `aria-describedby` so the error is announced, not just colored.

## Accessibility

Every field is keyboard-navigable with a visible 3px focus ring and respects your theme's contrast. Label every input — a bound `Label`, or an `aria-label` when the design has no visible label (as in the inline button and search examples). Disabled inputs set the `disabled` attribute (not just a class) so they leave the tab order, and icon toggles like show/hide password carry an `aria-label` describing the action.

## Theming

The input reads your design tokens (`--input`, `--ring`, `--destructive`, `--muted-foreground`), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.

## Installation

### input

**Install:**

```bash
npx shadcn@latest add @designrevision/input
```

**Dependencies:** utils

**Usage & accessibility:** Extends React.ComponentProps<"input"> — all native <input> attributes are supported. Drive state with type, disabled, readOnly, and aria-invalid (which switches on the destructive ring and border).

```tsx
// components/ui/input.tsx
import * as React from "react"

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

function Input({ className, type, ...props }: React.ComponentProps<"input">) {
  return (
    <input
      type={type}
      data-slot="input"
      className={cn(
        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
        className
      )}
      {...props}
    />
  )
}

export { Input }
```

## Examples

### With label

The canonical form field — a `Label` bound to the input by `htmlFor`, with muted helper text below.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-label-01
```

**Dependencies:** input, label

```tsx
// components/ui/input-label-01.tsx
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function InputLabel01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="email">Email</Label>
      <Input id="email" type="email" placeholder="you@example.com" />
      <p className="text-sm text-muted-foreground">
        We&apos;ll only use this to send you a receipt.
      </p>
    </div>
  )
}
```

---

### Search

A leading search icon and a trailing `⌘K` hint built with the `Kbd` component. The icon is `pointer-events-none` so clicks fall through to the field.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-search-01
```

**Dependencies:** lucide-react, input, kbd

```tsx
// components/ui/input-search-01.tsx
import { Search } from "lucide-react"

import { Input } from "@/components/ui/input"
import { Kbd } from "@/components/ui/kbd"

export default function InputSearch01() {
  return (
    <div className="relative w-full max-w-sm">
      <Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
      <Input type="search" placeholder="Search…" className="pl-9 pr-12" />
      <Kbd className="absolute right-2 top-1/2 -translate-y-1/2">⌘K</Kbd>
    </div>
  )
}
```

---

### Password

A show/hide toggle that swaps the input `type` between `password` and `text` — built with a ghost icon `Button` and an `aria-label`.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-password-01
```

**Dependencies:** lucide-react, input, label, button

```tsx
// components/ui/input-password-01.tsx
"use client"

import * as React from "react"
import { Eye, EyeOff } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function InputPassword01() {
  const [visible, setVisible] = React.useState(false)

  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="password">Password</Label>
      <div className="relative">
        <Input
          id="password"
          type={visible ? "text" : "password"}
          placeholder="Enter your password"
          className="pr-10"
        />
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          onClick={() => setVisible((v) => !v)}
          aria-label={visible ? "Hide password" : "Show password"}
          className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
        >
          {visible ? <EyeOff /> : <Eye />}
        </Button>
      </div>
    </div>
  )
}
```

---

### With button

An inline field and a submit `Button` in one row — the pattern behind newsletter and waitlist sign-ups.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-button-01
```

**Dependencies:** input, button

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

export default function InputButton01() {
  return (
    <form className="flex w-full max-w-sm items-center gap-2">
      <Input type="email" placeholder="you@example.com" aria-label="Email" />
      <Button type="submit">Subscribe</Button>
    </form>
  )
}
```

---

### States

Invalid (`aria-invalid` switches on the destructive ring and border, paired with `aria-describedby`), disabled, and read-only fields.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-validation-01
```

**Dependencies:** input, label

```tsx
// components/ui/input-validation-01.tsx
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function InputValidation01() {
  return (
    <div className="grid w-full max-w-sm gap-6">
      <div className="grid gap-2">
        <Label htmlFor="email-invalid">Email</Label>
        <Input
          id="email-invalid"
          type="email"
          defaultValue="not-an-email"
          aria-invalid="true"
          aria-describedby="email-invalid-error"
        />
        <p id="email-invalid-error" className="text-sm text-destructive">
          Enter a valid email address.
        </p>
      </div>
      <div className="grid gap-2">
        <Label htmlFor="email-disabled">Disabled</Label>
        <Input
          id="email-disabled"
          type="email"
          placeholder="you@example.com"
          disabled
        />
      </div>
      <div className="grid gap-2">
        <Label htmlFor="email-readonly">Read only</Label>
        <Input
          id="email-readonly"
          type="email"
          defaultValue="you@example.com"
          readOnly
        />
      </div>
    </div>
  )
}
```

---

### File

A native `type="file"` input styled to match — the upload button comes from `file:` pseudo-element classes, no JavaScript replacement needed.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-file-01
```

**Dependencies:** input, label

```tsx
// components/ui/input-file-01.tsx
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function InputFile01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="attachment">Attachment</Label>
      <Input id="attachment" type="file" />
      <p className="text-sm text-muted-foreground">PDF or DOCX, up to 5 MB.</p>
    </div>
  )
}
```

---

### With add-ons

Leading and trailing text add-ons (`https://` … `.com`) wrap the field; the border and focus ring move to the group so it reads as one control.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-addon-01
```

**Dependencies:** input, label

```tsx
// components/ui/input-addon-01.tsx
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function InputAddon01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="site">Website</Label>
      <div className="flex h-9 w-full items-center rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 dark:bg-input/30">
        <span className="select-none pl-3 text-sm text-muted-foreground">
          https://
        </span>
        <Input
          id="site"
          type="text"
          placeholder="acme"
          className="h-auto border-0 bg-transparent px-1 shadow-none focus-visible:border-0 focus-visible:ring-0 dark:bg-transparent"
        />
        <span className="select-none pr-3 text-sm text-muted-foreground">
          .com
        </span>
      </div>
    </div>
  )
}
```

---

### Character counter

A live `current/max` count rendered in `tabular-nums`, backed by the input’s `maxLength`.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-counter-01
```

**Dependencies:** input, label

```tsx
// components/ui/input-counter-01.tsx
"use client"

import * as React from "react"

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

export default function InputCounter01() {
  const maxLength = 40
  const [value, setValue] = React.useState("")

  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="title">Title</Label>
      <div className="relative">
        <Input
          id="title"
          value={value}
          maxLength={maxLength}
          onChange={(e) => setValue(e.target.value)}
          placeholder="A short, descriptive title"
          className="pr-16"
        />
        <span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs tabular-nums text-muted-foreground">
          {value.length}/{maxLength}
        </span>
      </div>
    </div>
  )
}
```
