Input
A single-line text field with an optional leading icon and an invalid state.
stableInput.tsx
Examples
Basic
As it appears on your invoices.
input/basic.tsx
import { Input } from "@streamui/react";
export default function InputBasic() {
return (
<div className="w-full max-w-xs">
<label htmlFor="input-name" className="mb-2 block font-bold">
Full name
</label>
<Input id="input-name" autoComplete="name" placeholder="Ada Lovelace" aria-describedby="input-name-hint" />
<p id="input-name-hint" className="mt-2 text-sm text-ink-muted">
As it appears on your invoices.
</p>
</div>
);
}With icon
input/with-icon.tsx
import { Search } from "lucide-react";
import { Input } from "@streamui/react";
export default function InputWithIcon() {
return (
<div className="w-full max-w-xs">
<label htmlFor="input-search" className="sr-only">
Search projects
</label>
<Input id="input-search" type="search" icon={<Search />} placeholder="Search projects…" />
</div>
);
}Invalid and disabled
Enter a complete email address.
input/states.tsx
import { Mail } from "lucide-react";
import { Input } from "@streamui/react";
export default function InputStates() {
return (
<div className="flex w-full max-w-xs flex-col gap-6">
<div>
<label htmlFor="input-invalid" className="mb-2 block font-bold">
Work email
</label>
<Input
id="input-invalid"
type="email"
icon={<Mail />}
defaultValue="ada@"
invalid
aria-describedby="input-invalid-error"
/>
<p id="input-invalid-error" className="mt-2 text-sm font-semibold">
Enter a complete email address.
</p>
</div>
<div>
<label htmlFor="input-disabled" className="mb-2 block font-bold">
Workspace ID
</label>
<Input id="input-disabled" defaultValue="ws_8f2k1" disabled />
</div>
</div>
);
}Installation
bun add @streamui/reactusage.tsx
import { Input } from "@streamui/react";
<label htmlFor="email">Email</label>
<Input id="email" type="email" />API
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | ReactNode | — | Leading icon. Rendered aria-hidden. |
| invalid | boolean | false | Sets aria-invalid and tints the field pink. |
| frameClassName | string | — | Classes for the outer frame (border, width). |
| className | string | — | Classes for the <input> element itself. |
| ...props | ComponentProps<"input"> | — | Every native input attribute. |
Accessibility
- Always pair with a <label>; use sr-only on it when the design has no visible label.
- When invalid, link the error message with aria-describedby so it is read on focus.
- The frame shows the focus ring, since the input itself has no visible outline.
Source
packages/react/src/Input.tsx
import type { ComponentProps, ReactNode } from "react";
import { cn } from "./cn";
export type InputProps = ComponentProps<"input"> & {
/** Leading icon, e.g. `<Mail />`. Decorative; hidden from assistive tech. */
icon?: ReactNode;
/** Marks the field invalid (sets `aria-invalid`) and tints it. */
invalid?: boolean;
/** Classes for the outer frame. `className` goes to the `<input>`. */
frameClassName?: string;
};
export function Input({
icon,
invalid,
frameClassName,
className,
...props
}: InputProps) {
return (
<div
className={cn(
"flex items-center border-2 border-ink bg-paper-raised shadow-brutal-sm transition-shadow duration-150",
"focus-within:shadow-brutal focus-within:outline-3 focus-within:outline-offset-2 focus-within:outline-blue",
"has-disabled:opacity-50 has-disabled:shadow-none",
invalid && "bg-pink-soft",
frameClassName,
)}
>
{icon && (
<span aria-hidden className="ml-3 shrink-0 text-ink-muted [&_svg]:size-4.5">
{icon}
</span>
)}
<input
aria-invalid={invalid || undefined}
className={cn(
"h-11 w-full min-w-0 bg-transparent px-3 placeholder:text-ink-muted focus:outline-none disabled:cursor-not-allowed",
className,
)}
{...props}
/>
</div>
);
}