stream/ui
Browse the library

Comic Checkbox

A hand-inked checkbox whose check mark draws itself in with a starburst flash.

betaComicCheckbox.tsx

Examples

Basic

Check it: the mark draws in and a burst flashes behind the box.

comic-checkbox/basic.tsx
import { ComicCheckbox } from "@streamuilib/react";

export default function ComicCheckboxBasic() {
  return <ComicCheckbox label="Remember this device" />;
}

Group

Today’s missions
comic-checkbox/group.tsx
import { ComicCheckbox } from "@streamuilib/react";

export default function ComicCheckboxGroup() {
  return (
    <fieldset className="flex flex-col gap-4">
      <legend className="mb-4 font-bold">Today&rsquo;s missions</legend>
      <ComicCheckbox name="mission" value="cape" label="Iron the cape" defaultChecked />
      <ComicCheckbox name="mission" value="cat" label="Rescue the cat" />
      <ComicCheckbox name="mission" value="villain" label="Foil the villain" />
      <ComicCheckbox name="mission" value="lunch" label="Lunch (required)" checked disabled readOnly />
    </fieldset>
  );
}

Installation

bun add @streamuilib/react
Open in v0 (opens in a new tab)
usage.tsx
import { ComicCheckbox } from "@streamuilib/react";

<ComicCheckbox name="terms" label="I agree to the terms" />

API

PropTypeDefaultDescription
labelReactNode—Text next to the box; clicking it toggles the box.
...propsOmit<ComponentProps<"input">, "type">—Native checkbox attributes: checked, defaultChecked, name, value, disabled.

Accessibility

  • A real checkbox, visually hidden, so forms, Space to toggle, and screen readers work unchanged.
  • The focus ring shows on the box when the hidden input has keyboard focus.
  • The draw-in and flash stop when the user prefers reduced motion.

Source

packages/react/src/ComicCheckbox.tsx
import { cn } from "./cn";
import { halftone } from "./comic";
import { SHAPES } from "./ComicBadge";
import type { CheckboxProps } from "./Checkbox";

export type ComicCheckboxProps = CheckboxProps;

/**
 * A comic-book checkbox: a hand-inked box whose check mark draws itself in
 * with a starburst flash behind it. The real `<input>` stays in the DOM
 * (visually hidden), so forms, keyboard, and screen readers work unchanged.
 * Motion stops for reduced motion.
 */
export function ComicCheckbox({ label, className, ...props }: ComicCheckboxProps) {
  return (
    <label
      className={cn(
        "sui-comic-check inline-flex cursor-pointer items-center gap-3 has-disabled:cursor-not-allowed has-disabled:opacity-50",
        className,
      )}
    >
      <input type="checkbox" className="peer sr-only" {...props} />
      <span
        aria-hidden
        className={cn(
          "sui-comic-check-box relative grid size-7 shrink-0 -rotate-3 place-items-center rounded-[7px_3px_8px_3px/3px_8px_3px_7px] border-[3px] border-ink bg-paper-raised shadow-[3px_3px_0_var(--ink)]",
          "peer-checked:bg-lime peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-blue",
        )}
        style={halftone(12)}
      >
        <svg viewBox="0 0 100 100" overflow="visible" className="sui-comic-check-flash absolute -inset-3 -z-10 size-[calc(100%+1.5rem)] opacity-0">
          <path d={SHAPES.burst.d} className="fill-pink stroke-ink" strokeWidth={5} strokeLinejoin="round" />
        </svg>
        <svg viewBox="0 0 24 24" className="size-5 overflow-visible text-ink">
          <path className="sui-comic-check-mark" d="M4 13 L10 18 L21 4" pathLength={1} fill="none" stroke="currentColor" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </span>
      {label && <span className="font-bold">{label}</span>}
    </label>
  );
}