Accordion
Accordion shows and hides stacked sections of content. Use it when readers need only some sections of a long page. For a single section, use Collapsible.
'use client';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/nxds/components/accordion/accordion';
export default function AccordionDefault() {
return (
<Accordion
type="single"
collapsible
variant="stacked"
className="nx:w-full nx:max-w-md"
>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that match the nxds design system.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It has smooth open/close animations built-in.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
Installation
npx nxds-cli@latest add accordion
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/nxds/components/accordion/accordion';
Composition
Accordion
└── AccordionItem
├── AccordionTrigger
└── AccordionContent
Examples
Multiple
Open several sections at once with type="multiple".
'use client';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/nxds/components/accordion/accordion';
export default function AccordionMultiple() {
return (
<Accordion
type="multiple"
variant="stacked"
className="nx:w-full nx:max-w-md"
>
<AccordionItem value="item-1">
<AccordionTrigger>First Section</AccordionTrigger>
<AccordionContent>
This is the content for the first section. Multiple sections can be
open at the same time.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Second Section</AccordionTrigger>
<AccordionContent>
This is the content for the second section. Try opening both!
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Third Section</AccordionTrigger>
<AccordionContent>
This is the content for the third section.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
Floating
Show each section as a separate bordered card.
'use client';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/nxds/components/accordion/accordion';
export default function AccordionFloating() {
return (
<Accordion
type="single"
collapsible
variant="floating"
className="nx:w-full nx:max-w-md"
>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that match the nxds design system.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It has smooth open/close animations built-in.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
Disabled item
Disable one item so its section cannot open.
'use client';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/nxds/components/accordion/accordion';
export default function AccordionDisabledItem() {
return (
<Accordion
type="single"
collapsible
variant="stacked"
className="nx:w-full nx:max-w-md"
>
<AccordionItem value="item-1">
<AccordionTrigger>Enabled Item</AccordionTrigger>
<AccordionContent>This item can be toggled.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" disabled>
<AccordionTrigger>Disabled Item</AccordionTrigger>
<AccordionContent>This item cannot be toggled.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Another Enabled Item</AccordionTrigger>
<AccordionContent>This item can also be toggled.</AccordionContent>
</AccordionItem>
</Accordion>
);
}
Props
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "floating" | "stacked" | null | "stacked" | —No description |
AccordionContent
AccordionContent adds no props of its own; it accepts the props of the element or primitive it wraps.
AccordionItem
AccordionItem adds no props of its own; it accepts the props of the element or primitive it wraps.
AccordionTrigger
AccordionTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.
Accordion shows and hides stacked sections of content. Use it when readers need only some sections of a long page. For a single section, use Collapsible.
<script setup lang="ts">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion';
</script>
<template>
<Accordion type="single" collapsible class="nx:w-full nx:max-w-md">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent
>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent
>Yes. It comes with default styles that match the nxds design
system.</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent
>Yes. It has smooth open/close animations built-in.</AccordionContent
>
</AccordionItem>
</Accordion>
</template>
Installation
- 1
Install the following dependencies:
npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0 - 2
Copy and paste the following code into your project.
components/accordion/Accordion.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { AccordionRoot, type AccordionRootEmits, type AccordionRootProps, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { type AccordionVariants, accordionVariants } from '.'; export interface AccordionProps extends AccordionRootProps { variant?: AccordionVariants['variant']; class?: HTMLAttributes['class']; } const props = withDefaults(defineProps<AccordionProps>(), { variant: 'stacked', }); const emits = defineEmits<AccordionRootEmits>(); const delegated = reactiveOmit(props, 'class', 'variant'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <AccordionRoot v-slot="slotProps" v-bind="forwarded" data-slot="accordion" :data-variant="variant" :data-orientation="orientation ?? 'vertical'" :class="cn(accordionVariants({ variant }), props.class)" > <slot v-bind="slotProps" /> </AccordionRoot> </template>components/accordion/AccordionContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { AccordionContent, type AccordionContentProps as RekaAccordionContentProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; export interface AccordionContentProps extends RekaAccordionContentProps { class?: HTMLAttributes['class']; } const props = defineProps<AccordionContentProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <AccordionContent v-bind="forwarded" data-slot="accordion-content" class="nx:overflow-hidden nx:typography-body-default nx:text-muted-foreground nx:duration-default nx:data-[state=closed]:animate-accordion-up nx:data-[state=open]:animate-accordion-down" > <div :class="props.class"><slot /></div> </AccordionContent> </template>components/accordion/AccordionItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { AccordionItem, type AccordionItemProps as RekaAccordionItemProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface AccordionItemProps extends RekaAccordionItemProps { class?: HTMLAttributes['class']; } const props = defineProps<AccordionItemProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <AccordionItem v-slot="slotProps" v-bind="forwarded" data-slot="accordion-item" :class=" cn( 'nx:border-color-default nx:bg-container nx:px-4 nx:transition-colors nx:duration-fast nx:hover:bg-container-hover nx:data-disabled:border-color-disabled nx:data-disabled:text-disabled-foreground nx:data-disabled:hover:bg-container nx:data-[state=open]:pb-4', 'nx:group-data-[variant=stacked]/accordion:border-b-default nx:group-data-[variant=stacked]/accordion:last:border-b-0', 'nx:group-data-[variant=floating]/accordion:rounded-md nx:group-data-[variant=floating]/accordion:border-width-default', props.class ) " > <slot v-bind="slotProps" /> </AccordionItem> </template>components/accordion/AccordionTrigger.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps as RekaAccordionTriggerProps, injectAccordionItemContext, injectAccordionRootContext, } from 'reka-ui'; import { computed, type HTMLAttributes } from 'vue'; import { IconChevronDown } from '../../lib/icons'; import { cn } from '../../lib/utils'; defineOptions({ inheritAttrs: false }); export interface AccordionTriggerProps extends RekaAccordionTriggerProps { class?: HTMLAttributes['class']; } const props = defineProps<AccordionTriggerProps>(); const delegated = reactiveOmit(props, 'class'); const root = injectAccordionRootContext(); const item = injectAccordionItemContext(); // An open item that a click can't close reads as disabled, as Radix marks it. const locked = computed( () => item.open.value && root.isSingle.value && !root.collapsible ); </script> <template> <AccordionHeader class="nx:m-0 nx:flex"> <AccordionTrigger v-bind="{ ...delegated, ...$attrs }" data-slot="accordion-trigger" :aria-disabled="locked || undefined" :class=" cn( 'nx:flex nx:w-full nx:flex-1 nx:items-start nx:gap-4 nx:border-0 nx:bg-transparent nx:px-0 nx:py-4 nx:typography-label-default nx:text-foreground nx:transition-control nx:duration-fast nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground nx:disabled:[&>svg]:text-disabled-foreground nx:[&[data-state=open]>svg]:rotate-180', props.class ) " > <span data-slot="accordion-trigger-label" class="nx:min-w-0 nx:flex-1 nx:truncate nx:text-left" > <slot /> </span> <IconChevronDown class="nx:size-4 nx:shrink-0 nx:text-muted-foreground nx:transition-transform nx:duration-default" aria-hidden="true" /> </AccordionTrigger> </AccordionHeader> </template>components/accordion/index.ts import { cva, type VariantProps } from 'class-variance-authority'; export { default as Accordion, type AccordionProps } from './Accordion.vue'; export { default as AccordionContent, type AccordionContentProps, } from './AccordionContent.vue'; export { default as AccordionItem, type AccordionItemProps, } from './AccordionItem.vue'; export { default as AccordionTrigger, type AccordionTriggerProps, } from './AccordionTrigger.vue'; export const accordionVariants = cva( 'nx:group/accordion nx:flex nx:w-full nx:flex-col', { variants: { variant: { stacked: '', floating: 'nx:gap-2', }, }, defaultVariants: { variant: 'stacked', }, } ); export type AccordionVariants = VariantProps<typeof accordionVariants>;lib/icons.ts /** * Internal Icons * * This barrel file re-exports icons used internally by the design system. * These are hardcoded icons for component states (loading, validation, navigation). * * User-facing icons should be passed via props by consumers. * * This set draws Phosphor. `nxds init --icons` installs the same names drawn * by another library. */ import { PhCircle } from '@phosphor-icons/vue'; import { type FunctionalComponent, h } from 'vue'; // Loading states export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue'; // Navigation export { PhArrowDown as IconArrowDown, PhArrowLeft as IconArrowLeft, PhArrowRight as IconArrowRight, PhArrowUp as IconArrowUp, PhCaretDown as IconChevronDown, PhCaretLeft as IconChevronLeft, PhCaretRight as IconChevronRight, PhCaretUp as IconChevronUp, PhDotsThree as IconDots, PhSidebarSimple as IconLayoutSidebar, PhCaretUpDown as IconSelector, } from '@phosphor-icons/vue'; // Actions export { PhCheck as IconCheck, PhCopy as IconCopy, PhMinus as IconMinus, PhPencilSimple as IconPencil, PhPlus as IconPlus, PhX as IconX, } from '@phosphor-icons/vue'; export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) => h(PhCircle, { weight: 'fill', ...attrs }); // Filter field types export { PhCalendarBlank as IconCalendar, PhHash as IconHash, PhTextAa as IconLetterCase, PhListBullets as IconList, } from '@phosphor-icons/vue'; // Search / input export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue'; // Alerts/Status export { PhWarningCircle as IconAlertCircle, PhWarning as IconAlertTriangle, PhCheckCircle as IconCircleCheck, PhInfo as IconInfoCircle, } from '@phosphor-icons/vue'; // Layout export { PhDotsSixVertical as IconGripVertical, PhPalette as IconPalette, } from '@phosphor-icons/vue'; - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/accordion';
Examples
Multiple
Open several sections at once with type="multiple".
<script setup lang="ts">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion';
</script>
<template>
<Accordion type="multiple" class="nx:w-full nx:max-w-md">
<AccordionItem value="item-1">
<AccordionTrigger>First Section</AccordionTrigger>
<AccordionContent
>This is the content for the first section. Multiple sections can be
open at the same time.</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Second Section</AccordionTrigger>
<AccordionContent
>This is the content for the second section. Try opening
both!</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Third Section</AccordionTrigger>
<AccordionContent
>This is the content for the third section.</AccordionContent
>
</AccordionItem>
</Accordion>
</template>
Floating
Show each section as a separate bordered card.
<script setup lang="ts">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion';
</script>
<template>
<Accordion
type="single"
collapsible
variant="floating"
class="nx:w-full nx:max-w-md"
>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent
>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent
>Yes. It comes with default styles that match the nxds design
system.</AccordionContent
>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent
>Yes. It has smooth open/close animations built-in.</AccordionContent
>
</AccordionItem>
</Accordion>
</template>
Disabled item
Disable one item so its section cannot open.
<script setup lang="ts">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion';
</script>
<template>
<Accordion type="single" collapsible class="nx:w-full nx:max-w-md">
<AccordionItem value="item-1">
<AccordionTrigger>Enabled Item</AccordionTrigger>
<AccordionContent>This item can be toggled.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" disabled>
<AccordionTrigger>Disabled Item</AccordionTrigger>
<AccordionContent>This item cannot be toggled.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Another Enabled Item</AccordionTrigger>
<AccordionContent>This item can also be toggled.</AccordionContent>
</AccordionItem>
</Accordion>
</template>
Props
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "stacked" | "floating" | null | "stacked" | —No description |
AccordionContent
AccordionContent adds no props of its own; it accepts the props of the element or primitive it wraps.
AccordionItem
AccordionItem adds no props of its own; it accepts the props of the element or primitive it wraps.
AccordionTrigger
AccordionTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.