Accordion
A collapsible content panel for organizing information in a compact space
Usage
<script setup lang="ts">
import { Accordion } from '@hareui/vue'
</script><script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem { icon: string}const items: FaqItem[] = [ { content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.', icon: 'gravity-ui:shopping-bag', label: 'How do I place an order?', }, { content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.', icon: 'gravity-ui:receipt', label: 'Can I modify or cancel my order?', }, { content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.', icon: 'gravity-ui:credit-card', label: 'What payment methods do you accept?', }, { content: 'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.', icon: 'gravity-ui:box', label: 'How much does shipping cost?', }, { content: 'Yes, we ship to most countries. Please check our shipping rates and policies for more information.', icon: 'gravity-ui:planet-earth', label: 'Do you ship internationally?', }, { content: 'If you\'re not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.', icon: 'gravity-ui:arrows-rotate-left', label: 'How do I request a refund?', },]</script><template> <Accordion class="w-full max-w-md" :items="items"> <template #leading="{ item }"> <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span> </template> <template #indicator> <Icon height="16" icon="gravity-ui:chevron-down" width="16" /> </template> </Accordion></template>Anatomy
Accordion renders one item per items entry: a heading with a trigger button (leading content, label, indicator) and a collapsible panel holding the body. HeroUI's compound parts (Accordion.Item, Accordion.Heading, Accordion.Trigger, Accordion.Indicator, Accordion.Panel, Accordion.Body) become items, slots and ui keys:
<template>
<Accordion :items="items" v-model="expanded">
<template #leading="{ item, index, open }" /> <!-- before the label (e.g. an icon) -->
<template #label="{ item, index, open }" /> <!-- replaces item.label -->
<template #indicator="{ item, index, open }" /> <!-- Accordion.Indicator's children -->
<template #content="{ item, index, open }" /> <!-- fallback body content -->
<template #[item.slot]="{ item, index, open }" /> <!-- per-item body content -->
</Accordion>
</template>- A single root element in
#indicatorreceives the indicator classes anddata-expanded, like HeroUI'sAccordion.Indicatorchildren, so any icon rotates when its item expands. - Extra fields on an item (icons, subtitles, …) are passed through to the slots as
item. Accordion.Body'sclassNamemaps to thebodyInneruikey.
ui key | data-slot | Element |
|---|---|---|
base | accordion | root element |
item | accordion-item | one per item |
heading | accordion-heading | <h3> around the trigger |
trigger | accordion-trigger | the expand/collapse button |
indicator | accordion-indicator | chevron (or #indicator content) |
panel | accordion-panel | collapsible panel |
body | accordion-body | content body container |
bodyInner | – | padded wrapper inside the body (HeroUI's Accordion.Body className target) |
Typing items
items takes the exported AccordionItem type. Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.
import type { AccordionItem } from '@hareui/vue'
interface MyItem extends AccordionItem {
icon: string
}
const items: MyItem[] = [/* ... */]
// <template #leading="{ item }"> — item.icon is a stringExamples
Surface
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem { icon: string}const items: FaqItem[] = [ { content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.', icon: 'gravity-ui:shopping-bag', label: 'How do I place an order?', }, { content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.', icon: 'gravity-ui:receipt', label: 'Can I modify or cancel my order?', }, { content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.', icon: 'gravity-ui:credit-card', label: 'What payment methods do you accept?', }, { content: 'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.', icon: 'gravity-ui:box', label: 'How much does shipping cost?', }, { content: 'Yes, we ship to most countries. Please check our shipping rates and policies for more information.', icon: 'gravity-ui:planet-earth', label: 'Do you ship internationally?', }, { content: 'If you\'re not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.', icon: 'gravity-ui:arrows-rotate-left', label: 'How do I request a refund?', },]</script><template> <Accordion class="w-full max-w-md" :items="items" variant="surface"> <template #leading="{ item }"> <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span> </template> <template #indicator> <Icon height="16" icon="gravity-ui:chevron-down" width="16" /> </template> </Accordion></template>Without Separator
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqItem extends AccordionItem { icon: string}const items: FaqItem[] = [ { content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.', icon: 'gravity-ui:shopping-bag', label: 'How do I place an order?', }, { content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.', icon: 'gravity-ui:receipt', label: 'Can I modify or cancel my order?', }, { content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.', icon: 'gravity-ui:credit-card', label: 'What payment methods do you accept?', },]</script><template> <Accordion class="w-full max-w-md" :items="items" hide-separator> <template #leading="{ item }"> <span class="me-3 size-4 shrink-0 text-muted"><Icon :icon="item.icon" height="16" width="16" /></span> </template> <template #indicator> <Icon height="16" icon="gravity-ui:chevron-down" width="16" /> </template> </Accordion></template>Multiple Expanded
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Accordion } from '@hareui/vue'const items: AccordionItem[] = [ { label: 'Getting Started', content: 'Learn the basics of HeroUI and how to integrate it into your React project. This section covers installation, setup, and your first component.', }, { label: 'Core Concepts', content: 'Understand the fundamental concepts behind HeroUI, including the compound component pattern, styling with Tailwind CSS, and accessibility features.', }, { label: 'Advanced Usage', content: 'Explore advanced features like custom variants, theme customization, and integration with other libraries in your React ecosystem.', }, { label: 'Best Practices', content: 'Follow our recommended best practices for building performant, accessible, and maintainable applications with HeroUI components.', },]</script><template> <Accordion class="w-full max-w-md" :items="items" multiple /></template>Disabled State
Entire accordion disabled
Individual items disabled
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Accordion } from '@hareui/vue'const disabledItems: AccordionItem[] = [ { label: 'Disabled Item 1', content: 'This content cannot be accessed when the accordion is disabled.' }, { label: 'Disabled Item 2', content: 'This content cannot be accessed when the accordion is disabled.' },]const items: AccordionItem[] = [ { label: 'Active Item', content: 'This item is active and can be toggled normally.' }, { label: 'Disabled Item', content: 'This content cannot be accessed when the item is disabled.', disabled: true }, { label: 'Another Active Item', content: 'This item is also active and can be toggled.' },]</script><template> <div class="flex w-full flex-col items-center gap-8"> <div class="w-full max-w-md space-y-2"> <h3 class="text-sm font-medium text-muted"> Entire accordion disabled </h3> <Accordion class="w-full max-w-md" disabled :items="disabledItems" /> </div> <div class="w-full max-w-md space-y-2"> <h3 class="text-sm font-medium text-muted"> Individual items disabled </h3> <Accordion class="w-full max-w-md" :items="items" /> </div> </div></template>Controlled
Expanded: getting-started
<script setup lang="ts">import type { AccordionItem, ExpandedValue } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion, Button, useDisclosureGroupNavigation } from '@hareui/vue'import { ref } from 'vue'interface Section extends AccordionItem { value: string}const items: Section[] = [ { content: 'Learn the basics of HeroUI and how to integrate it into your React project. This section covers installation, setup, and your first component.', value: 'getting-started', label: 'Getting Started', }, { content: 'Understand the fundamental concepts behind HeroUI, including the compound component pattern, styling with Tailwind CSS, and accessibility features.', value: 'core-concepts', label: 'Core Concepts', }, { content: 'Explore advanced features like custom variants, theme customization, and integration with other libraries in your React ecosystem.', value: 'advanced-usage', label: 'Advanced Usage', },]const expandedKeys = ref<ExpandedValue[]>(['getting-started'])const { isNextDisabled, isPrevDisabled, onNext, onPrevious } = useDisclosureGroupNavigation({ expandedKeys, itemIds: items.map(item => item.value), onExpandedChange: (keys) => { expandedKeys.value = keys },})</script><template> <div class="w-full max-w-md"> <div class="mb-4 flex items-center justify-between"> <p class="text-sm text-muted"> Expanded: <strong>{{ expandedKeys.join(', ') || 'none' }}</strong> </p> <div class="flex gap-2"> <Button aria-label="Previous item" :disabled="isPrevDisabled" size="sm" variant="secondary" @click="onPrevious"> <Icon class="size-4" icon="gravity-ui:chevron-up" /> </Button> <Button aria-label="Next item" :disabled="isNextDisabled" size="sm" variant="secondary" @click="onNext"> <Icon class="size-4" icon="gravity-ui:chevron-down" /> </Button> </div> </div> <Accordion v-model="expandedKeys" :items="items" /> </div></template>Custom Indicator
<script setup lang="ts">import type { AccordionItem, ExpandedValue } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'import { ref } from 'vue'interface IndicatorItem extends AccordionItem { icon?: string}const items: IndicatorItem[] = [ { value: '1', label: 'Using Plus/Minus Icon', content: 'This accordion uses a plus icon that transforms when expanded. The icon automatically rotates 45 degrees to form an X.', }, { value: '2', label: 'Using Caret Icon', icon: 'gravity-ui:circle-chevron-down', content: 'This item uses a caret icon for the indicator. The rotation animation is applied automatically.', }, { value: '3', label: 'Using Arrow Icon', icon: 'gravity-ui:chevrons-down', content: 'This item uses an arrow icon. Any icon you pass will receive the rotation animation when the item expands.', },]const expandedKeys = ref<ExpandedValue[]>([''])</script><template> <Accordion v-model="expandedKeys" class="w-full max-w-md" :items="items" variant="surface"> <template #indicator="{ item, open }"> <Icon v-if="item.icon" height="16" :icon="item.icon" width="16" /> <Icon v-else height="16" :icon="open ? 'gravity-ui:minus' : 'gravity-ui:plus'" width="16" /> </template> </Accordion></template>FAQ Layout
Frequently Asked Questions
Everything you need to know about licensing and usage.
General
Licensing
Support
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface FaqCategory { title: string items: AccordionItem[]}const categories: FaqCategory[] = [ { items: [ { content: 'Browse our products, add items to your cart, and proceed to checkout. You\'ll need to provide shipping and payment information to complete your purchase.', label: 'How do I place an order?', }, { content: 'Yes, you can modify or cancel your order before it\'s shipped. Once your order is processed, you can\'t make changes.', label: 'Can I modify or cancel my order?', }, ], title: 'General', }, { items: [ { content: 'You can purchase a license directly from our website. Select the license type that fits your needs and proceed to checkout.', label: 'How do I purchase a license?', }, { content: 'A standard license is for personal use or small projects, while a pro license includes commercial use rights and priority support.', label: 'What is the difference between a standard and a pro license?', }, ], title: 'Licensing', }, { items: [ { content: 'You can reach our support team through the contact form on our website, or email us directly at [email protected].', label: 'How do I get support?', }, ], title: 'Support', },]</script><template> <div class="flex w-full flex-col gap-6"> <div class="flex flex-col gap-1"> <h2 class="text-2xl font-bold"> Frequently Asked Questions </h2> <p class="mb-4 text-lg font-medium text-muted"> Everything you need to know about licensing and usage. </p> </div> <div v-for="category in categories" :key="category.title"> <p class="text-md mb-2 font-medium text-muted"> {{ category.title }} </p> <Accordion class="w-full" :items="category.items" variant="surface"> <template #indicator> <Icon height="16" icon="gravity-ui:chevron-down" width="16" /> </template> </Accordion> </div> </div></template>Customization
Tailwind CSS
<script setup lang="ts">import type { AccordionItem } from '@hareui/vue'import { Icon } from '@iconify/vue'import { Accordion } from '@hareui/vue'interface SetupItem extends AccordionItem { iconUrl: string subtitle: string}const items: SetupItem[] = [ { content: 'Stay informed about your account activity with real-time notifications.', iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/bell-small.png', subtitle: 'Receive account activity updates', label: 'Set Up Notifications', }, { content: 'Enhance your browsing experience by installing our official browser extension', iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/compass-small.png', subtitle: 'Connect your browser to your account', label: 'Set up Browser Extension', }, { content: 'Begin your journey into the world of digital collectibles by creating your first NFT. ', iconUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/3dicons/mint-collective-small.png', subtitle: 'Create your first collectible', label: 'Mint Collectible', },]const ui = { item: "group/item first:**:data-[slot=accordion-trigger]:rounded-t-2xl last:[&:not(:has([data-slot=accordion-trigger][aria-expanded='true']))_[data-slot=accordion-trigger]]:rounded-b-2xl", trigger: 'group flex items-center gap-2 transition-none hover:bg-surface', indicator: 'text-muted/50 [&>svg]:size-4', bodyInner: 'text-muted/80',}</script><template> <Accordion class="bg-surface-1/10 w-full max-w-md rounded-2xl" :items="items" :ui="ui" variant="surface"> <template #leading="{ item }"> <img :alt="item.label" class="h-11 w-11 transition-[scale,rotate] duration-300 ease-out group-hover/item:scale-120 group-hover/item:-rotate-10 group-hover/item:drop-shadow-lg" :src="item.iconUrl" > </template> <template #label="{ item }"> <div class="flex flex-col gap-0"> <span class="leading-5 font-medium">{{ item.label }}</span> <span class="leading-6 font-normal text-muted/80">{{ item.subtitle }}</span> </div> </template> <template #indicator> <Icon height="16" icon="gravity-ui:chevron-down" width="16" /> </template> </Accordion></template>Global Configuration
To customize the Accordion classes for the whole app, extend the accordion theme in createHareUI:
app.use(createHareUI({
ui: {
accordion: {
slots: {
base: 'rounded-xl bg-gray-50',
trigger: 'font-semibold text-lg',
},
variants: {
variant: {
surface: { base: 'shadow-lg border-2' },
},
},
},
},
}))Styling Reference
Slots
base→[data-slot="accordion"]– base accordion containerbody→[data-slot="accordion-body"]– content body containerbodyInner– padded wrapper inside the bodyheading→[data-slot="accordion-heading"]– heading wrapperindicator→[data-slot="accordion-indicator"]– expand/collapse indicator iconitem→[data-slot="accordion-item"]– individual accordion itempanel→[data-slot="accordion-panel"]– collapsible panel containertrigger→[data-slot="accordion-trigger"]– clickable trigger button
Variants
variant="surface"– surface background with rounded corners
State Attributes
[data-slot="accordion-trigger"][aria-expanded="true"]– expanded state[data-slot="accordion-panel"][aria-hidden="false"]– panel visible state
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveron trigger - Focus:
:focus-visibleon trigger - Disabled:
:disabledon trigger,[data-disabled="true"]on item - Expanded:
[aria-expanded="true"]on trigger,[data-expanded="true"]on item, panel and indicator
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItem[] | - | The items to render. |
modelValue | ExpandedValue[] | undefined | The expanded item values. Use v-model to control them. |
defaultValue | ExpandedValue[] | - | The values expanded on first render (uncontrolled). |
multiple | boolean | false | Whether multiple items can be expanded at once (HeroUI's allowsMultipleExpanded). |
disabled | boolean | false | Whether the entire accordion is disabled. |
variant | "surface" | "default" | 'default' | The visual variant of the accordion. |
hideSeparator | boolean | false | Hide separator lines between accordion items. |
ui | ComponentSlots<{ slots: { base: string; body: string; bodyInner: string; heading: string; indicator: string[]; item: string[]; panel: string[]; trigger: string[]; }; variants: { variant: { default: {}; surface: { base: string; trigger: string; item: string[]; }; }; }; defaultVariants: { variant: string; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
leading | ItemSlotProps | Rendered in the trigger before the label (e.g. an icon). |
label | ItemSlotProps | Replaces the trigger's label text. |
indicator | ItemSlotProps | Replaces the chevron (HeroUI's Accordion.Indicator children). A single root element
receives the themed indicator classes and data-expanded, so it rotates like the default. |
content | ItemSlotProps | Body content for every item, overridden by [item.slot]. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: ExpandedValue[]] | - |
useDisclosureGroupNavigation
Previous/next navigation for a controlled Accordion or DisclosureGroup (used by the Controlled example).
| Option | Type | Default | Description |
|---|---|---|---|
expandedKeys | MaybeRefOrGetter<ExpandedValue[]> | - | The current v-model value |
itemIds | MaybeRefOrGetter<ExpandedValue[]> | - | Every item's value, in display order |
onExpandedChange | (keys: ExpandedValue[]) => void | - | Called with the new expanded values |
multiple | MaybeRefOrGetter<boolean> | false | Keep previously expanded items open while navigating |
Returns currentIndex, isPrevDisabled, isNextDisabled (computed refs) and onPrevious, onNext.



