Comic Input
A comic-book text field: hand-inked corners, halftone dots, and an Oops! sticker when invalid.
betaComicInput.tsx
Examples
Basic
With a label, an icon, or both. The frame lifts toward the light on focus.
comic-input/basic.tsx
import { Mail, Search } from "lucide-react";
import { ComicInput } from "@streamuilib/react";
export default function ComicInputBasic() {
return (
<div className="flex w-full max-w-sm flex-col gap-5">
<label className="flex flex-col gap-2">
<span className="font-bold">Your hero name</span>
<ComicInput placeholder="Captain Brutal" />
</label>
<label className="flex flex-col gap-2">
<span className="font-bold">Email</span>
<ComicInput type="email" icon={<Mail />} placeholder="you@example.com" />
</label>
<ComicInput type="search" icon={<Search />} aria-label="Search issues" placeholder="Search issues…" />
</div>
);
}Invalid and disabled
Invalid tints the frame and slams on a sticker; set your own word with sfx.
comic-input/states.tsx
import { ComicInput } from "@streamuilib/react";
export default function ComicInputStates() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<label className="flex flex-col gap-2">
<span className="font-bold">Username</span>
<ComicInput defaultValue="no spaces allowed" invalid aria-describedby="comic-user-error" />
<span id="comic-user-error" className="text-sm font-semibold text-ink">
Use letters, numbers, and dashes only.
</span>
</label>
<label className="flex flex-col gap-2">
<span className="font-bold">Coupon</span>
<ComicInput defaultValue="NOPE" invalid sfx="Nope!" />
</label>
<label className="flex flex-col gap-2">
<span className="font-bold">Workspace</span>
<ComicInput defaultValue="stream-ui" disabled />
</label>
</div>
);
}Installation
bun add @streamuilib/reactusage.tsx
import { ComicInput } from "@streamuilib/react";
<label htmlFor="name">Hero name</label>
<ComicInput id="name" placeholder="Captain Brutal" />API
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | ReactNode | — | Leading icon. Rendered aria-hidden. |
| invalid | boolean | false | Sets aria-invalid, tints the frame pink, and slaps on the sticker. |
| sfx | ReactNode | false | "Oops!" | The sticker shown when invalid; false for none. |
| frameClassName | string | — | Classes for the outer frame (width, tilt). |
| ...props | ComponentProps<"input"> | — | Every native input attribute; className goes to the <input>. |
Accessibility
- A real <input>: pair it with a <label>, and link error text with aria-describedby.
- The sticker is decorative and hidden; aria-invalid carries the state.
- The frame shows the focus ring and the lift; motion stops for reduced motion.
Source
packages/react/src/ComicInput.tsx
import type { ReactNode } from "react";
import { cn } from "./cn";
import { comicFrame, halftone, stampClass } from "./comic";
import type { InputProps } from "./Input";
export type ComicInputProps = InputProps & {
/** The sticker slapped on when invalid. Defaults to "Oops!"; false for none. */
sfx?: ReactNode | false;
};
/**
* A comic-book text input: a thick ink frame with uneven, hand-inked corners,
* halftone dots, and a hard shadow that lifts on focus. When invalid it tints
* pink and an "Oops!" sticker slams on. Same props as Input.
*/
export function ComicInput({ icon, invalid, sfx = "Oops!", frameClassName, className, ...props }: ComicInputProps) {
return (
<div className={comicFrame({ invalid, className: cn("items-center", frameClassName) })} style={halftone(10)}>
{icon && (
<span aria-hidden className="ml-3 shrink-0 text-ink [&_svg]:size-4.5">
{icon}
</span>
)}
<input
aria-invalid={invalid || undefined}
className={cn(
"h-11 w-full min-w-0 bg-transparent px-3 font-semibold placeholder:font-normal placeholder:text-ink-muted focus:outline-none disabled:cursor-not-allowed",
className,
)}
{...props}
/>
{invalid && sfx && (
<span aria-hidden className={stampClass}>
{sfx}
</span>
)}
</div>
);
}