htsx

Accordion

Accordion component for Hono JSX

Examples

What is this?
According component for htsx.
Does it need JavaScript?
No. Powered by <details>/<summary> only.
How do I close all items?
Click the currently open item again.
Feature A
Multiple items can be open at the same time.
Feature B
Pass variant="multiple" to enable this behavior.
Feature C
Use defaultOpen to pre-open specific items.
JSX title
  • content accepts JSX.
  • Icons, lists, images, buttons, etc.
Another JSX title

Inline elements like bold and italic work too.

// single (default): only one item open at a time, defaultOpen can be set
<Accordion
  defaultOpen={["q1"]}
  items={[
    {
      value: "q1",
      title: "What is this?",
      content: "According component for htsx.",
    },
    // more items
/>
 
// multiple: several items can be open at once
<Accordion
  variant="multiple"
  defaultOpen={["f1", "f3"]}
  items={[
    {
      value: "f1",
      title: "Feature A",
      content: "Multiple items can be open at the same time.",
    },
    // more items
  ]}
/>
 
// title and content accept JSX
<Accordion
  class="rounded-md border border-border bg-card text-card-foreground"
  items={[
    {
      value: "jsx1",
      title: (
        <span class="flex items-center gap-2">
          <Sun />
          JSX title
        </span>
      ),
      content: (
        <ul>
          <li>
            <code>content</code> accepts JSX.
          </li>
          <li>Icons, lists, images, buttons, etc.</li>
        </ul>
      ),
    },
    // more items
  ]}
/>

Code

ui/accordion.tsx
import type { JSX, Child } from "hono/jsx";
import { c } from "./c";
import { ChevronDown } from "./icons";
 
export function Accordion({
  id,
  variant = "single",
  defaultOpen = [],
  items,
  class: custom,
  ...props
}: JSX.IntrinsicElements["div"] & {
  variant?: "single" | "multiple";
  defaultOpen?: string[];
  items: {
    value: string;
    title: Child;
    content: Child;
  }[];
}) {
  const groupName = variant === "single" ? (id ?? crypto.randomUUID()) : undefined;
  return (
    <div id={id} class={c("w-full divide-y divide-border", custom)} {...props}>
      {items.map((item) => (
        <details
          key={item.value}
          name={groupName}
          open={defaultOpen.includes(item.value) || undefined}
          class="group/accordion-item"
        >
          <summary class="flex cursor-pointer list-none items-center justify-between gap-4 px-4 py-3 font-medium select-none hover:opacity-80">
            {item.title}
            <span class="shrink-0 text-muted-foreground transition-transform group-open/accordion-item:rotate-180">
              <ChevronDown />
            </span>
          </summary>
          <div class="px-4 pb-4 text-muted-foreground">{item.content}</div>
        </details>
      ))}
    </div>
  );
}
ui/icons.tsx
export const ChevronDown = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="lucide lucide-chevron-down-icon lucide-chevron-down"
  >
    <path d="m6 9 6 6 6-6" />
  </svg>
);