InputNew
pnpm dlx shadcn@latest add …/r/input.jsonAn accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
import { Input } from "@/components/ui/input"
export function EmailField() {
return (
<>
<label htmlFor="email">Email</label>
<Input id="email" type="email" placeholder="you@example.com" />
</>
)
}import { Input } from "@/components/ui/input";
import { useId } from "react";
export default function InputDefault() {
const id = useId();
return (
<div className="grid w-full max-w-xs gap-2">
<label htmlFor={id} className="text-sm font-medium">
Email
</label>
<Input id={id} type="email" placeholder="you@example.com" autoComplete="email" />
</div>
);
}Installation
Install the dependencies.
$ pnpm add class-variance-authorityAdd the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.
:root { --size-control-sm: 2rem; --size-control-md: 2.25rem; --size-control-lg: 2.5rem; --size-icon-sm: 0.875rem; --size-icon-md: 1rem; --size-icon-lg: 1.25rem; --duration-instant: 80ms; --duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 350ms; --duration-slower: 500ms; --z-popover: 1400; --focus-ring-width: 2px; --focus-ring-offset: 2px; --opacity-disabled: 0.5; --scale-press: 0.97; --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1); --destructive-foreground: oklch(1 0 0); } .dark { --destructive-foreground: oklch(1 0 0); } @theme inline { --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both; --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both; --color-destructive-foreground: var(--destructive-foreground); @keyframes popover-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } @keyframes popover-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } } } @media (prefers-reduced-motion: reduce) { :root { --duration-instant: 1ms; --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; --duration-slower: 1ms; } }Copy the source into your project.
import { cn } from "@/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; import type { ComponentProps } from "react"; const inputVariants = cva( [ "flex w-full min-w-0 rounded-lg border border-input bg-transparent px-3 text-sm text-foreground shadow-xs", "transition duration-(--duration-fast) ease-out", "placeholder:text-muted-foreground", "outline-ring focus-visible:border-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)", "aria-invalid:border-destructive aria-invalid:outline-destructive", "disabled:cursor-not-allowed disabled:opacity-(--opacity-disabled)", "file:me-3 file:h-full file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground", ], { variants: { size: { sm: "h-(--size-control-sm) rounded-md px-2.5", md: "h-(--size-control-md)", lg: "h-(--size-control-lg)", }, }, defaultVariants: { size: "md" }, }, ); /** `size` is the height variant here, so the native numeric `size` attribute is left out */ export type InputProps = Omit<ComponentProps<"input">, "size"> & VariantProps<typeof inputVariants>; /** * A text field. Pair it with a visible label (`<label htmlFor>`), and set `aria-invalid` with an * error message linked through `aria-describedby` when the value is wrong. */ function Input({ className, size, type = "text", ...props }: InputProps) { return ( <input data-slot="input" data-size={size ?? "md"} type={type} className={cn(inputVariants({ size }), className)} {...props} /> ); } export { Input, inputVariants };Update the import paths to match your project.
Examples
Invalid
Set aria-invalid and link the error message with aria-describedby, so screen readers announce it.
Use at least 3 characters.
import { Input } from "@/components/ui/input";
import { useId } from "react";
export default function InputInvalid() {
const id = useId();
return (
<div className="grid w-full max-w-xs gap-2">
<label htmlFor={id} className="text-sm font-medium">
Username
</label>
<Input id={id} defaultValue="f" aria-invalid="true" aria-describedby={`${id}-error`} />
<p id={`${id}-error`} className="text-sm text-destructive">
Use at least 3 characters.
</p>
</div>
);
}Sizes
Small, medium and large, matching Button's heights.
import { Input } from "@/components/ui/input";
export default function InputSizes() {
return (
<div className="grid w-full max-w-xs gap-3">
<Input size="sm" placeholder="Small" aria-label="Small input" />
<Input placeholder="Medium" aria-label="Medium input" />
<Input size="lg" placeholder="Large" aria-label="Large input" />
</div>
);
}Disabled
import { Input } from "@/components/ui/input";
import { useId } from "react";
export default function InputDisabled() {
const id = useId();
return (
<div className="grid w-full max-w-xs gap-2">
<label htmlFor={id} className="text-sm font-medium">
Workspace
</label>
<Input id={id} defaultValue="fewtone" disabled />
</div>
);
}File
The file picker button takes the input's styles.
import { Input } from "@/components/ui/input";
import { useId } from "react";
export default function InputFile() {
const id = useId();
return (
<div className="grid w-full max-w-xs gap-2">
<label htmlFor={id} className="text-sm font-medium">
Avatar
</label>
<Input id={id} type="file" accept="image/*" />
</div>
);
}API reference
Also accepts every native input prop except the numeric size attribute. Exports inputVariants.
| Prop | Type | Default | Description |
|---|---|---|---|
size? | "sm" | "md" | "lg" | "md" | The height. |
type? | string | "text" | The native input type. |
aria-invalid? | boolean | – | Marks the value as wrong: the border and focus ring turn destructive. |
disabled? | boolean | false | Prevents editing. |
Related components
- SelectNew
An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.
- SwitchNew
An accessible React switch for on/off settings, with a spring-animated thumb, built on Radix Switch.
- ButtonNew
An accessible React button with six variants, three sizes, icon support and a spring press, built on a native button.