stream/ui
Browse the library

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/react
Open in v0 (opens in a new tab)
usage.tsx
import { ComicInput } from "@streamuilib/react";

<label htmlFor="name">Hero name</label>
<ComicInput id="name" placeholder="Captain Brutal" />

API

PropTypeDefaultDescription
iconReactNode—Leading icon. Rendered aria-hidden.
invalidbooleanfalseSets aria-invalid, tints the frame pink, and slaps on the sticker.
sfxReactNode | false"Oops!"The sticker shown when invalid; false for none.
frameClassNamestring—Classes for the outer frame (width, tilt).
...propsComponentProps<"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>
  );
}