stream/ui
Browse the library

Comic Accordion

Hand-inked panels with a starburst toggle that spins open; the answer pops in on halftone dots.

betaComicAccordion.tsx

Examples

Basic

Is it really free?
Yes! MIT licensed. Use it in anything, no strings attached.
Does it need React 19?
React 18 or 19 both work. The styles come from Tailwind CSS v4.
Is there a dark mode?
Not yet. Comics are printed on paper, after all.
comic-accordion/basic.tsx
import { ComicAccordion } from "@streamuilib/react";

export default function ComicAccordionBasic() {
  return (
    <ComicAccordion
      className="w-full max-w-lg"
      defaultOpen={["free"]}
      items={[
        { id: "free", title: "Is it really free?", content: "Yes! MIT licensed. Use it in anything, no strings attached." },
        { id: "react", title: "Does it need React 19?", content: "React 18 or 19 both work. The styles come from Tailwind CSS v4." },
        { id: "dark", title: "Is there a dark mode?", content: "Not yet. Comics are printed on paper, after all." },
      ]}
    />
  );
}

One at a time

Chapter 1: The spark
Opening one chapter closes the others.
Chapter 2: The fall
Only one is open at a time, using the native details name attribute.
Chapter 3: The comeback
No JavaScript needed.
comic-accordion/exclusive.tsx
import { ComicAccordion } from "@streamuilib/react";

export default function ComicAccordionExclusive() {
  return (
    <ComicAccordion
      exclusive
      className="w-full max-w-lg"
      items={[
        { id: "1", title: "Chapter 1: The spark", content: "Opening one chapter closes the others." },
        { id: "2", title: "Chapter 2: The fall", content: "Only one is open at a time, using the native details name attribute." },
        { id: "3", title: "Chapter 3: The comeback", content: "No JavaScript needed." },
      ]}
    />
  );
}

Installation

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

<ComicAccordion
  items={[
    { id: "free", title: "Is it free?", content: "Yes! MIT licensed." },
  ]}
/>

API

PropTypeDefaultDescription
items{ id: string; title: ReactNode; content: ReactNode }[]—Sections in order. id must be unique.
exclusivebooleanfalseOnly one item open at a time.
defaultOpenstring[][]Ids of items open on first render.
classNamestring—Classes for the wrapper.

Accessibility

  • Native <details> and <summary>: Enter and Space toggle, screen readers hear expanded or collapsed, and find-in-page opens matches.
  • The starburst toggle is decorative and hidden.
  • The spin and pop-in stop when the user prefers reduced motion.

Source

packages/react/src/ComicAccordion.tsx
import { useId } from "react";
import { Plus } from "lucide-react";
import { cn } from "./cn";
import { halftone, sketchy } from "./comic";
import { SHAPES } from "./ComicBadge";
import type { AccordionProps } from "./Accordion";

export type ComicAccordionProps = AccordionProps;

/** Each item's hand-placed tilt, repeating. */
const TILTS = [-0.6, 0.5, -0.3, 0.7];

/**
 * A comic-book accordion: hand-inked panels with shouty titles and a
 * starburst toggle that spins as it opens; the answer pops in on halftone
 * dots. Built on native <details>/<summary>, like Accordion: keyboard
 * support, screen reader state, find-in-page, and no JavaScript needed.
 */
export function ComicAccordion({ items, exclusive = false, defaultOpen = [], className }: ComicAccordionProps) {
  const group = useId();

  return (
    <div className={cn("flex flex-col gap-4", className)}>
      {items.map((item, i) => (
        <details
          key={item.id}
          name={exclusive ? group : undefined}
          open={defaultOpen.includes(item.id)}
          className={cn(
            "sui-comic-acc group border-[3px] border-ink bg-paper-raised text-ink shadow-[4px_4px_0_var(--ink)] open:bg-lime-soft",
            sketchy,
          )}
          style={{ rotate: `${TILTS[i % TILTS.length]}deg` }}
        >
          <summary className="flex cursor-pointer list-none items-center justify-between gap-4 px-5 py-3.5 font-bold [&::-webkit-details-marker]:hidden">
            {item.title}
            <span aria-hidden className="sui-comic-acc-burst relative grid size-10 shrink-0 place-items-center">
              <svg viewBox="0 0 100 100" overflow="visible" className="absolute inset-0 size-full">
                <path d={SHAPES.burst.d} className="fill-paper-raised stroke-ink group-open:fill-pink" strokeWidth={6} strokeLinejoin="round" />
              </svg>
              <Plus className="relative size-4" strokeWidth={3} />
            </span>
          </summary>
          <div className="sui-comic-acc-body border-t-[3px] border-dashed border-ink px-5 py-4 leading-relaxed" style={halftone(10)}>
            {item.content}
          </div>
        </details>
      ))}
    </div>
  );
}