Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

Examples

Basic

PropDefaultTypeDescription
items[]arraySet the accordion items.
item.value-stringThe unique value of the item used for rendering and state tracking
item.label-stringThe label of the accordion item
item.content-stringThe content of the accordion item
Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Mounted

PropDefaultTypeDescription
unmountOnHidetruebooleanSet to false to keep the content of the accordion mounted, even if the accordion is closed.
item.unmountOnHidetruebooleanLike unmountOnHide, but applies to a specific item. Has no effect if unmountOnHide is false

⚡ By default, the accordion's content is not rendered until it is opened for performance reasons. To render the content when the page loads, even if the accordion is closed for SEO purposes, set the unmountOnHide prop to false.

Preview
Code

Multiple

PropDefaultTypeDescription
typesingle'single' | 'multiple'Expand a single item or multiple items at a time
Preview
Code

Default open

PropDefaultTypeDescription
defaultValue-string | string[]Open the specified items whose value matches the input.
Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Color

Since we use the Button component for the accordion label, you can use _accordionTrigger prop to pass button props, like btn to change the color of the label. See Button for more information.

Preview
Code

Icon

PropDefaultTypeDescription
_accordionTrigger.leading-stringSet a custom leading icon for the accordion.
_accordionTrigger.trailing-stringSet a custom trailing icon for the accordion.
item._accordionTrigger.leading-stringSet a custom leading icon for a specific item.
item._accordionTrigger.trailing-stringSet a custom trailing icon for a specific item.
Preview
Code
Custom Global leading icons

Custom per item leading icon

Custom Trailing open icon

Custom Trailing open and close icons

Reverse

PropDefaultTypeDescription
_accordionTrigger.reverse-booleanSwitch the position of the trailing and leading icons.
item._accordionTrigger.reverse-booleanSwitch the position of the trailing and leading icons of a specific item.
Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Variant

PropDefaultTypeDescription
accordionborder divider{variant}The variant of the accordion.
VariantDescription
borderA bordered accordion.
dividerAn accordion with dividers between items.
~An unstyled accordion.
Preview
Code

Customization

You can customize the accordion using the una prop and utility classes.

Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.


Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.


Preview
Code

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Slots

NamePropsDescription
default{ modelValue }Fill with AccordionItem components
item{ open, item, index }The item of the accordion.
header{ open, item, index }The header of the accordion containing the trigger
trigger{ open, item, index }The trigger button.
content{ open, item, index }The content of the accordion.
Slot propDescription
modelValuethe value of the item(s) currently open
openallows you to access the open state of the item.
itemallows you to access the item object.
indexallows you to access the index of the item. To identify items, prefer using item.value.
Preview
Code


All slots can be prefixed with the value of an item to target a specific item. For example, to customize the content of the item with the value 1, use the 1-content slot.

Slot nameDescription
${value}The render slot of a specific item. Default value renders the other item's slots.
${value}-headerThe header of a specific item
${value}-triggerThe trigger of a specific item
${value}-contentThe content of a specific item
Preview
Code

Presets

shortcuts/accordion.ts
type AccordionPrefix = 'accordion'

export const staticAccordion: Record<`${AccordionPrefix}-${string}` | AccordionPrefix, string> = {
  // config
  'accordion-trailing-icon': 'i-lucide-chevron-up group-data-[state=open]/accordion-trigger:-rotate-180 group-data-[state=closed]/accordion-trigger:rotate-0 transition-transform duration-300',
  'accordion-divider': 'divide-(y base)',
  'accordion-border': 'border-(~ base) rounded-md',

  // base
  'accordion': 'flex-(~ col) relative w-full',
  'accordion-content': 'flex overflow-hidden',
  'accordion-item': 'w-full overflow-hidden focus-within:(relative z-10 shadow)',
  'accordion-header': 'flex',
  'accordion-trigger': 'w-full flex justify-between items-center accordion-trigger-padding',
  'accordion-trigger-padding': 'p-(x-3 y-4) is-[.btn]:h-auto',
  'accordion-trigger-label': 'flex-1 text-left',
  'accordion-panel': 'text-(muted 0.875em) border-(t $c-divider) accordion-trigger-padding',
  'accordion-leading': 'text-1.2em',
  'accordion-trailing': 'flex transition items-center text-1em duration-300',
}

export const dynamicAccordion = [
]

export const accordion = [
  ...dynamicAccordion,
  staticAccordion,
]

Props

types/accordion.ts
import type { AccordionContentProps, AccordionHeaderProps, AccordionItemProps, AccordionRootProps, AccordionTriggerProps } from 'reka-ui'
import type { NButtonProps } from './button'

export interface NAccordionProps extends AccordionRootProps {
  /**
   * Allows you to add `UnaUI` accordion preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * By default, we don't add any options or variants to the accordion,
   * But you can add your own in the configuration file.
   */
  accordion?: string

  /**
   * List of items to be rendered,
   * It extends the `NButtonProps` interface
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/nuxt/src/runtime/types/button.ts
   */
  items?: NAccordionItemProps[]

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/accordion.ts
   */
  una?: NAccordionUnaProps & NButtonProps['una']

  _accordionItem?: Omit<NAccordionItemProps, 'una' | 'value'>
  _accordionHeader?: Omit<NAccordionHeaderProps, 'una'>
  _accordionTrigger?: Omit<NButtonProps, 'una'>
  _accordionContent?: Omit<NAccordionContentProps, 'una'>
}

export interface NAccordionContentProps extends AccordionContentProps {
  una?: Pick<NAccordionUnaProps, 'accordionContent' | 'accordionPanel'>
}

export interface NAccordionHeaderProps extends AccordionHeaderProps {
  una?: Pick<NAccordionUnaProps, 'accordionHeader' | 'accordionTrigger' | 'accordionTrailing' | 'accordionTrailingClose' | 'accordionTrailingOpen' | 'accordionLeading'>
}

export interface NAccordionTriggerProps extends AccordionTriggerProps, NButtonProps {
  una?: Pick<NAccordionUnaProps, 'accordionTrigger' | 'accordionTrailing' | 'accordionTrailingClose' | 'accordionTrailingOpen' | 'accordionLeading'> & NButtonProps['una']
}

export interface NAccordionItemProps extends AccordionItemProps {
  label?: string
  /**
   * Accordion item content
   */
  content?: string

  una?: Omit<NAccordionUnaProps, 'accordion'> & NButtonProps['una']

  _accordionHeader?: Omit<NAccordionHeaderProps, 'una'>
  _accordionTrigger?: Omit<NAccordionTriggerProps, 'una'>
  _accordionContent?: Omit<NAccordionContentProps, 'una'>
}

interface NAccordionUnaProps {
  accordion?: string
  accordionItem?: string
  accordionTrailing?: string
  accordionTrailingOpen?: string
  accordionTrailingClose?: string
  accordionLeading?: string
  accordionHeader?: string
  accordionTrigger?: string
  accordionContent?: string
  accordionPanel?: string
}

Components

Accordion.vue
<script setup lang="ts">
import type { AccordionRootEmits } from 'reka-ui'

import type { NAccordionProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import { defu } from 'defu'
import { AccordionRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../../utils'
import NAccordionItem from './AccordionItem.vue'

const props = withDefaults(defineProps<NAccordionProps>(), {
  collapsible: true,
  type: 'single',
  accordion: 'divider border',
})

const emits = defineEmits<AccordionRootEmits>()

const rootProps = useForwardPropsEmits(reactiveOmit(props, ['una', 'items', '_accordionTrigger', '_accordionContent', '_accordionHeader', '_accordionItem']), emits)

const items = computed(() => {
  if (import.meta.dev) {
    const reservedValues = ['header', 'trigger', 'content', 'item', 'default']
    for (const item of props.items ?? []) {
      if (reservedValues.includes(item.value)) {
        console.warn(`[AccordionItem]: The value '${item.value}' is reserved and may cause unexpected behavior. Please choose a different value.`)
      }
    }
  }
  return props.items
})
</script>

<template>
  <AccordionRoot
    v-slot="{ modelValue }"
    v-bind="rootProps"
    :class="cn(
      una?.accordion,
    )"
  >
    <slot :model-value>
      <NAccordionItem
        v-for="(item, index) in items"
        :key="item.value"
        v-bind="defu(item, _accordionItem)"
        :_accordion-trigger="defu(item._accordionTrigger, _accordionTrigger)"
        :_accordion-content="defu(item._accordionContent, _accordionContent)"
        :_accordion-header="defu(item._accordionHeader, _accordionHeader)"
        :una="defu(item.una, una)"
      >
        <template #default="{ open }">
          <slot :name="`${item.value}-item`" :open :item :index>
            <slot name="item" :open :item :index />
          </slot>
        </template>

        <template #header="{ open }">
          <slot :name="`${item.value}-header`" :open :item :index>
            <slot name="header" :open :item :index />
          </slot>
        </template>

        <template #trigger="{ open }">
          <slot :name="`${item.value}-trigger`" :open :item :index>
            <slot name="trigger" :open :item :index />
          </slot>
        </template>

        <template #content="{ open }">
          <slot :name="`${item.value}-content`" :open :item :index>
            <slot name="content" :open :item :index />
          </slot>
        </template>
      </NAccordionItem>
    </slot>
  </AccordionRoot>
</template>
<script setup lang="ts">
import type { NAccordionItemProps } from '../../../types/accordion'
import { reactiveOmit } from '@vueuse/core'
import { AccordionItem, Primitive, useForwardProps } from 'reka-ui'
import { cn } from '../../../utils'
import NAccordionContent from './AccordionContent.vue'
import NAccordionHeader from './AccordionHeader.vue'
import NAccordionTrigger from './AccordionTrigger.vue'

const props = defineProps<NAccordionItemProps>()
const forwardProps = useForwardProps(reactiveOmit(props, ['una', 'label', 'content', '_accordionContent', '_accordionHeader', '_accordionTrigger']))
</script>

<template>
  <AccordionItem
    v-slot="{ open }"
    v-bind="forwardProps"
    :class="cn('accordion-item', una?.accordionItem)"
  >
    <slot :open>
      <NAccordionHeader :una v-bind="_accordionHeader">
        <Primitive
          as-child
          :label
          v-bind="_accordionTrigger"
          :una="{
            btnLeading: cn('accordion-leading', una?.accordionLeading),
            btnTrailing: cn(
              'accordion-trailing',
              una?.accordionTrailing,
              open ? una?.accordionTrailingOpen : una?.accordionTrailingClose,
            ),
          }"
        >
          <slot name="header" :open>
            <NAccordionTrigger>
              <slot name="trigger" :open />
            </NAccordionTrigger>
          </slot>
        </Primitive>
      </NAccordionHeader>
      <NAccordionContent :una v-bind="_accordionContent">
        <slot name="content" :open>
          {{ content }}
        </slot>
      </NAccordionContent>
    </slot>
  </AccordionItem>
</template>
<script setup lang="ts">
import type { NAccordionHeaderProps } from '../../../types/accordion'
import { reactiveOmit } from '@vueuse/core'
import { AccordionHeader, useForwardProps } from 'reka-ui'
import { cn } from '../../../utils'

const props = defineProps<NAccordionHeaderProps>()
const forwardProps = useForwardProps(reactiveOmit(props, ['una']))
</script>

<template>
  <AccordionHeader v-bind="forwardProps" :class="cn('accordion-header', una?.accordionHeader)">
    <slot />
  </AccordionHeader>
</template>
<script setup lang="ts">
import type { NAccordionTriggerProps } from '../../../types/accordion'
import { reactiveOmit } from '@vueuse/core'
import { AccordionTrigger, useForwardProps } from 'reka-ui'
import { cn } from '../../../utils'

const props = withDefaults(defineProps<NAccordionTriggerProps>(), {
  btn: '~ text',
  trailing: 'accordion-trailing-icon',
})
const forwardProps = useForwardProps(reactiveOmit(props, ['una']))
</script>

<template>
  <AccordionTrigger
    class="group/accordion-trigger accordion-trigger"
    v-bind="forwardProps"
    as-child
    :una="{
      ...una,
      btn: cn('accordion-trigger-padding', una?.btn),
      btnLabel: cn('accordion-trigger-label', una?.btnLabel),
    }"
  >
    <slot>
      <NButton />
    </slot>
  </AccordionTrigger>
</template>
<script setup lang="ts">
import type { NAccordionContentProps } from '../../../types/accordion'
import { reactiveOmit } from '@vueuse/core'
import { AccordionContent, useForwardProps } from 'reka-ui'
import { cn } from '../../../utils'

const props = defineProps<NAccordionContentProps>()
const forwardProps = useForwardProps(reactiveOmit(props, ['una']))
</script>

<template>
  <AccordionContent v-bind="forwardProps" :class="cn('accordion-content group/accordion-content', una?.accordionContent)">
    <div :class="cn('accordion-panel', una?.accordionPanel)">
      <slot />
    </div>
  </AccordionContent>
</template>

<style scoped>
.accordion-content[data-state='open'] {
  animation: accordionIn 300ms cubic-bezier(0.86, 0, 0.07, 1);
}
.accordion-content[data-state='closed'] {
  animation: accordionOut 300ms cubic-bezier(0.86, 0, 0.07, 1);
}
@keyframes accordionIn {
  from {
    height: 0;
  }
  to {
    height: var(--reka-accordion-content-height);
  }
}
@keyframes accordionOut {
  from {
    height: var(--reka-accordion-content-height);
  }
  to {
    height: 0;
  }
}
</style>