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.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
No props documented yet.
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.
Packages
Props
No props documented yet.
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="[email protected]" />
<p className="text-sm text-muted-foreground">
We'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.
Packages
Props
No props documented yet.
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`.
Packages
Props
No props documented yet.
"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.
Packages
Props
No props documented yet.
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="[email protected]" 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.
Packages
Props
No props documented yet.
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="[email protected]"
disabled
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email-readonly">Read only</Label>
<Input
id="email-readonly"
type="email"
defaultValue="[email protected]"
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.
Packages
Props
No props documented yet.
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.
Packages
Props
No props documented yet.
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`.
Packages
Props
No props documented yet.
"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>
)
}
The Shadcn Input is the foundation of every form in your interface — the canonical shadcn/ui input, a native <input> element themed entirely with CSS variables. It stays deliberately minimal so it composes with everything: pair it with a Label, wrap it with an icon or a Button, or drop in a 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.