# Accordion Smooth Height — React Accordion Component

> A single-open accordion that animates to its content's natural height while the chevron turns and the answer settles in from a blur.

- Category: [Accordions](https://pixelpapa.com/components/accordions.md)
- Tags: accordion, height, disclosure, faq, keyboard
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/accordions/accordion-smooth-height

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/accordion-smooth-height.json"
```

The file is written to `components/pixelpapa/accordion-smooth-height.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ChevronDown } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type AccordionItem = { id: string; title: string; content: ReactNode };

type AccordionProps = {
  items: AccordionItem[];
  /** Id of the item that starts open. */
  defaultOpen?: string | null;
  className?: string;
};

export function Accordion({ items, defaultOpen = null, className }: AccordionProps) {
  const uid = useId();
  const [open, setOpen] = useState<string | null>(defaultOpen);
  const headers = useRef<(HTMLButtonElement | null)[]>([]);
  const reduce = useReducedMotion();

  function onKeyDown(e: KeyboardEvent, i: number) {
    const last = items.length - 1;
    const next = e.key === "ArrowDown" ? (i === last ? 0 : i + 1) : e.key === "ArrowUp" ? (i === 0 ? last : i - 1) : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
    if (next < 0) return;
    e.preventDefault();
    headers.current[next]?.focus();
  }

  return (
    <div className={cn("divide-y divide-border overflow-hidden rounded-3xl border border-border bg-card", className)}>
      {items.map((item, i) => {
        const isOpen = open === item.id;
        return (
          <div key={item.id}>
            <h3>
              <button
                ref={(el) => {
                  headers.current[i] = el;
                }}
                id={`${uid}-h-${item.id}`}
                type="button"
                aria-expanded={isOpen}
                aria-controls={`${uid}-p-${item.id}`}
                onClick={() => setOpen(isOpen ? null : item.id)}
                onKeyDown={(e) => onKeyDown(e, i)}
                className="group flex w-full items-center justify-between gap-4 px-6 py-[18px] text-left outline-none transition-colors hover:bg-muted/50 focus-visible:bg-muted"
              >
                <span className={cn("font-medium transition-colors", isOpen ? "text-primary" : "text-card-foreground")}>{item.title}</span>
                <span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full transition-[background-color,color,rotate] duration-300", isOpen ? "rotate-180 bg-primary text-primary-foreground" : "bg-muted text-muted-foreground group-hover:text-foreground")}>
                  <ChevronDown className="size-4" />
                </span>
              </button>
            </h3>
            <AnimatePresence initial={false}>
              {isOpen && (
                <motion.div
                  id={`${uid}-p-${item.id}`}
                  role="region"
                  aria-labelledby={`${uid}-h-${item.id}`}
                  initial={{ height: 0 }}
                  animate={{ height: "auto" }}
                  exit={{ height: 0 }}
                  transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 320, damping: 36 }}
                  className="overflow-hidden"
                >
                  <motion.div
                    initial={{ opacity: 0, y: -10, filter: "blur(6px)" }}
                    animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                    exit={{ opacity: 0, y: -6, filter: "blur(4px)" }}
                    transition={{ duration: 0.28 }}
                    className="px-6 pb-5 text-sm leading-relaxed text-pretty text-muted-foreground"
                  >
                    {item.content}
                  </motion.div>
                </motion.div>
              )}
            </AnimatePresence>
          </div>
        );
      })}
    </div>
  );
}

const faqs: AccordionItem[] = [
  { id: "trial", title: "How does the 14-day trial work?", content: "You get every Studio feature with no card on file. On day 14 the workspace drops to the free plan automatically, and nothing you built is deleted." },
  { id: "seats", title: "Can I change the number of seats mid-cycle?", content: "Yes. Added seats are prorated to the day, and removed seats turn into credit on your next invoice. Viewers are always free." },
  { id: "data", title: "Where is our data stored?", content: "In the region you pick at sign-up: Frankfurt, Virginia or Sydney. Backups stay in the same region and are encrypted with a key unique to your workspace." },
  { id: "export", title: "What happens if we decide to leave?", content: "Export everything as CSV or JSON from Settings in one click. We keep your workspace for 30 days in case you change your mind, then erase it for good." },
];

export default function AccordionSmoothHeightDemo() {
  return (
    <div className="h-[400px] w-full max-w-lg">
      <Accordion items={faqs} defaultOpen="trial" />
    </div>
  );
}
```

## AI prompt

```text
Add the "Accordion Smooth Height" component from PixelPapa UI to this project.

What it is: A single-open accordion that animates to its content's natural height while the chevron turns and the answer settles in from a blur.
Category: Accordions. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/accordion-smooth-height.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/accordion-smooth-height.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/accordion-smooth-height" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More accordions

- [Accordion Horizontal Panels](https://pixelpapa.com/components/accordions/accordion-horizontal-panels.md) (Pro): Side-by-side panels where the chosen one springs wide to reveal its story while the others fold down to a vertical title.
- [Accordion Nested Tree](https://pixelpapa.com/components/accordions/accordion-nested-tree.md) (Pro): A nested tree with indent guides where branches unfold with animated height and staggered children, fully navigable with the arrow keys.
- [Accordion Numbered Editorial](https://pixelpapa.com/components/accordions/accordion-numbered-editorial.md): A magazine-style accordion with oversized serif numerals, a rule that draws across the open row and a plus that folds into a minus.
- [Accordion With Icons Cards](https://pixelpapa.com/components/accordions/accordion-with-icons-cards.md): Separate accordion cards with icon tiles; the open card lifts on a spring, its icon fills with colour and the plus turns into a close mark.
