ephemera

Accordion

Index-card dividers that open to reveal their notes.

Two to three days, weather and pigeons permitting.

Install

$ npx shadcn@latest add https://ephemera.kerrshift.com/r/accordion.json

Or copy the source below into components/ui/accordion.tsx. Core components need the Paper Theme and Radix where noted.

Usage

example.tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";

<Accordion type="single" collapsible defaultValue="shipping">
  <AccordionItem value="shipping">
    <AccordionTrigger>How long does post take?</AccordionTrigger>
    <AccordionContent>Two to three days, weather permitting.</AccordionContent>
  </AccordionItem>
  <AccordionItem value="returns">
    <AccordionTrigger>Can I send it back?</AccordionTrigger>
    <AccordionContent>Write "return to sender" on the front.</AccordionContent>
  </AccordionItem>
</Accordion>

Built on the Radix Accordion, so keyboard navigation and screen reader support come for free.

Props

PropTypeDefaultDescription
type"single" | "multiple"—One open at a time, or many.
collapsibleboolean—Allow closing the open item (single only).
value / defaultValue / onValueChangestring | string[]—Which items are open.

Any other props are passed to the root element.

Fonts

Components read optional CSS variables and fall back to system fonts: --font-display, --font-receipt, --font-type, --font-hand and --font-stamp. This site uses Fraunces, IBM Plex Mono, Courier Prime, Caveat and Anton.

Source

components/ui/accordion.tsx
"use client";

import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui";

/** Index-card dividers that open to reveal their notes. */
export const Accordion = AccordionPrimitive.Root;

export function AccordionItem({ className = "", ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return <AccordionPrimitive.Item className={`border-b border-dashed border-(--edge) last:border-b-0 ${className}`} {...props} />;
}

export function AccordionTrigger({ className = "", style, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        className={`group flex flex-1 cursor-pointer items-center justify-between gap-4 py-4 text-left text-[12.5px] font-bold uppercase tracking-[0.16em] text-(--ink) outline-none transition-colors hover:text-(--pen) focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-(--pen) disabled:pointer-events-none disabled:opacity-50 ${className}`}
        style={{ fontFamily: "var(--font-type, 'Courier New', ui-monospace, monospace)", ...style }}
        {...props}
      >
        {children}
        <span aria-hidden className="shrink-0 text-[18px] font-normal leading-none text-(--ink-muted) transition-transform duration-200 group-data-[state=open]:rotate-45">
          +
        </span>
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  );
}

export function AccordionContent({ className = "", children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-paper-motion
      className="overflow-hidden text-[15px] leading-relaxed text-(--ink-muted) data-[state=closed]:animate-[paper-accordion-up_.2s_ease-out] data-[state=open]:animate-[paper-accordion-down_.2s_ease-out]"
      {...props}
    >
      <div className={`pb-4 ${className}`}>{children}</div>
    </AccordionPrimitive.Content>
  );
}