Accordion
Migration guide for Accordion from HeroUI v2 to v3
Refer to the v3 Accordion documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.
Structure Changes
In v2, AccordionItem was a self-contained component that accepted props for title, subtitle, content, and other elements:
import { Accordion, AccordionItem } from "@heroui/react";
export default function App() {
return (
<Accordion>
<AccordionItem
key="1"
title="Accordion 1"
>
Content here
</AccordionItem>
<AccordionItem
key="2"
title="Accordion 2"
>
Content here
</AccordionItem>
</Accordion>
);
}In v3, Accordion uses a compound component pattern with explicit subcomponents:
import { Accordion } from "@heroui/react";
export default function App() {
return (
<Accordion>
<Accordion.Item id="1">
<Accordion.Heading>
<Accordion.Trigger>
Accordion 1
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content here</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="2">
<Accordion.Heading>
<Accordion.Trigger>
Accordion 2
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content here</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}Key Changes
1. Component Structure
v2: Single AccordionItem component with props
v3: Compound components: Accordion.Item, Accordion.Heading, Accordion.Trigger, Accordion.Panel, Accordion.Body, Accordion.Indicator
2. Prop Changes
| v2 Prop | v3 Location | Notes |
|---|---|---|
selectedKeys | Accordion | Renamed to expandedKeys; type Iterable<Key> |
defaultSelectedKeys | Accordion | Renamed to defaultExpandedKeys |
onSelectionChange | Accordion | Renamed to onExpandedChange; (keys: Set<Key>) => void |
selectionMode="multiple" | Accordion | Use allowsMultipleExpanded (boolean) |
isCompact | - | Removed (use Tailwind CSS e.g. text-sm, py-2) |
hideIndicator | - | Omit <Accordion.Indicator /> to hide |
disableAnimation, disableIndicatorAnimation, motionProps | - | Removed (animations not supported / use CSS) |
showDivider, dividerProps | - | Removed (add dividers manually or use Divider) |
keepContentMounted | - | Removed (content always mounted in v3) |
selectionBehavior, disallowEmptySelection | - | Removed (not applicable) |
itemClasses | - | Use className on items |
startContent | - | Place content in <Accordion.Trigger> |
title, subtitle | - | Place content in <Accordion.Trigger> |
3. Variants
v2 Variants: light, shadow, bordered, splitted
v3 Variants: default, surface
The v3 variants are simplified. To achieve similar effects to v2 variants:
- v2
light→ v3default - v2
shadow→ v3surface - v2
bordered→ v3default+ add border classes - v2
splitted→ v3default+ add background color and spacing/margin between items
4. Item Identification
v2: React's key was used for both list reconciliation and expanded state.
v3: Use id on Accordion.Item for expanded state; keep React's key on items in lists.
Migration Examples
Controlled State
import { useState } from "react";
import { Accordion, AccordionItem } from "@heroui/react";
const [selectedKeys, setSelectedKeys] = useState(new Set(["1"]));
<Accordion
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
<AccordionItem key="1" title="Item 1">
Content 1
</AccordionItem>
<AccordionItem key="2" title="Item 2">
Content 2
</AccordionItem>
</Accordion>import { useState } from "react";
import { Accordion } from "@heroui/react";
import type { Key } from "@heroui/react";
const [expandedKeys, setExpandedKeys] = useState<Set<Key>>(new Set(["1"]));
<Accordion
expandedKeys={expandedKeys}
onExpandedChange={setExpandedKeys}
>
<Accordion.Item id="1">
<Accordion.Heading>
<Accordion.Trigger>
Item 1
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content 1</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="2">
<Accordion.Heading>
<Accordion.Trigger>
Item 2
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content 2</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
</Accordion>With Subtitle and Start Content
import { Icon } from "@iconify/react";
<Accordion>
<AccordionItem
key="1"
title="Accordion 1"
subtitle="Press to expand"
startContent={<Icon icon="gravity-ui:box" />}
>
Content here
</AccordionItem>
</Accordion>import { Icon } from "@iconify/react";
<Accordion>
<Accordion.Item id="1">
<Accordion.Heading>
<Accordion.Trigger>
<Icon icon="gravity-ui:box" className="mr-2" />
<div className="flex flex-col">
<span>Accordion 1</span>
<span className="text-sm text-muted">Press to expand</span>
</div>
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content here</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
</Accordion>Custom Indicator
import { Icon } from "@iconify/react";
<Accordion>
<AccordionItem
key="1"
title="Item 1"
indicator={(props) => (
props.isOpen ? <Icon icon="mdi:minus" /> : <Icon icon="mdi:plus" />
)}
>
Content
</AccordionItem>
</Accordion>import { Icon } from "@iconify/react";
import { useState } from "react";
import { Accordion } from "@heroui/react";
import type { Key } from "@heroui/react";
const [expandedKeys, setExpandedKeys] = useState<Set<Key>>(new Set());
<Accordion
expandedKeys={expandedKeys}
onExpandedChange={setExpandedKeys}
>
<Accordion.Item id="1">
<Accordion.Heading>
<Accordion.Trigger>
Item 1
<Accordion.Indicator>
{expandedKeys.has("1") ? (
<Icon icon="gravity-ui:minus" />
) : (
<Icon icon="gravity-ui:plus" />
)}
</Accordion.Indicator>
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
</Accordion>Disabled Items and Default Expanded Keys
<Accordion
defaultExpandedKeys={["1"]}
disabledKeys={["2"]}
>
<AccordionItem key="1" title="Item 1">
Content 1
</AccordionItem>
<AccordionItem key="2" title="Item 2">
Content 2
</AccordionItem>
</Accordion><Accordion defaultExpandedKeys={["1"]}>
<Accordion.Item id="1">
<Accordion.Heading>
<Accordion.Trigger>
Item 1
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content 1</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="2" isDisabled>
<Accordion.Heading>
<Accordion.Trigger>
Item 2
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>Content 2</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
</Accordion>Styling Changes
v2: classNames Prop
<AccordionItem
classNames={{
base: "custom-base",
title: "custom-title",
content: "custom-content"
}}
/>v3: Direct className Props
<Accordion.Item className="custom-base">
<Accordion.Heading>
<Accordion.Trigger className="custom-title">
Title
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body className="custom-content">
Content
</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>Component Anatomy
The v3 Accordion follows this structure:
Accordion (Root)
└── Accordion.Item
├── Accordion.Heading
│ └── Accordion.Trigger
│ ├── [Your content: title, subtitle, icons, etc.]
│ └── Accordion.Indicator (optional)
└── Accordion.Panel
└── Accordion.Body
└── [Your content]Summary
- Component Structure: Must use compound components instead of props
- State Props:
selectedKeys→expandedKeys,onSelectionChange→onExpandedChange - Multiple Selection:
selectionMode="multiple"→allowsMultipleExpanded={true} - Item identity: Use
idfor expanded state; keep React'skeyfor list reconciliation - Variants: Reduced from 4 to 2 variants
- Removed Props: Many convenience props removed; use Tailwind CSS classes instead
- Content Structure: Title, subtitle, and start content must be manually placed in
Trigger - Indicator: Must be explicitly rendered; no automatic indicator