Accordion

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

Examples

Basic

PropDefaultTypeDescription
items-arraySet the accordion items.
Preview
Code
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vel urna vitae lectus aliquet mollis et eget risus.

Mounted

PropDefaultTypeDescription
mountedfalsebooleanMount the content of the accordion when the page loads, even if the accordion is closed.
item.mountfalsebooleanMount the content of a specific item when the page loads, even if the accordion is closed.

⚡ 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, use the mounted prop.

Preview
Code

Multiple

PropDefaultTypeDescription
multiplefalsebooleanExpand multiple items.
Preview
Code

Default open

PropDefaultTypeDescription
defaultOpenfalsebooleanOpen all items by default.
item.defaultOpenfalsebooleanOpen a specific item by default.
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.
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 the btn prop to change the color of the label. See Button for more information.

Preview
Code

Icon

PropDefaultTypeDescription
leading-stringAdd leading icon to the label.
item.leading-stringAdd leading icon to the label of a specific item.
trailingOpen-stringCustomize trailing open icon.
trailingClose-stringCustomize trailing close icon.
Preview
Code
Custom Global leading icons
Custom per item leading icon
Custom Trailing open icon
Custom Trailing open and close icons

Reverse

PropDefaultTypeDescription
reverse-booleanSwitch the position of the trailing and leading icons.
item.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.

Unstyle mode

PropDefaultTypeDescription
unstyle-booleanRemove the default border, padding, and divider of the 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
label{prop}The label of the accordion.
content{prop}The content of the accordion.
Slot propDescription
indexallows you to access index of the item.
itemallows you to access the item properties.
openallows you to access the open state of the item.
closeallows you to access the close state of the item.
Preview
Code

index number - allows you to customize the specific item of the accordion. See the example below.

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',
  'accordion-button-padding': 'p-(x-3 y-4)',
  'accordion-button-default-variant': 'btn-text',
  'accordion-divider': 'divide-(y base)',
  'accordion-border': 'border-(~ base) rounded-md',

  // base
  'accordion': 'flex-(~ col) relative w-full',
  'accordion-item': 'w-full',
  'accordion-button': 'justify-start',
  'accordion-panel': 'text-(muted 0.875em) border-(t $c-divider) accordion-button-padding',
  'accordion-leading': 'text-1.2em',
  'accordion-trailing': 'flex transition items-center text-1em duration-300',
  'accordion-label': 'flex w-full text-1em',

  // trailing transition
  'accordion-trailing-open': '-rotate-180',
  'accordion-trailing-close': 'rotate-0',

  // panel transition
  'accordion-enter-active': 'overflow-hidden transition-height duration-300',
  'accordion-leave-active': 'overflow-hidden transition-height duration-300',
}

export const dynamicAccordion = [
]

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

Props

types/accordion.ts
import type { NButtonProps } from './button'

export interface NAccordionProps extends Omit<NButtonProps, 'una'> {
  /**
   * 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
  /**
   * Update leading icon when accordion button item is open,
   * Accepts icon name and utility classes
   */
  trailingOpen?: string
  /**
   * Update leading icon when accordion button item is closed,
   * Accepts icon name and utility classes
   */
  trailingClose?: string

  /**
   * Allow multiple accordion items to be open at the same time
   *
   * @default false
   */
  multiple?: boolean
  /**
   * Allow accordion item to be open by default
   *
   * @default false
   */
  defaultOpen?: boolean
  /**
   * Removes border and divider from accordion
   *
   * @default false
   */
  unstyle?: boolean
  /**
   * By default, the accordion is unmounted for performance reasons,
   * This means that the accordion will not be rendered until it is opened,
   * If you want to render the accordion when the page loads, you can use the `mounted` prop.
   *
   * @default false
   */
  mounted?: boolean

  /**
   * 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?: {
    accordion?: string
    accordionItem?: string
    accordionButton?: string
    accordionPanel?: string
    accordionLeading?: string
    accordionTrailing?: string
    accordionTrailingOpen?: string
    accordionTrailingClose?: string
    accordionEnterActive?: string
    accordionLeaveActive?: string
  } & NButtonProps['una']
}

export interface NAccordionItemProps extends NButtonProps {
  /**
   * Accordion item content
   */
  content?: string
  /**
   * Update item leading icon when accordion button item is open,
   * Accepts icon name and utility classes
   *
   * @example
   * trailingOpen='i-heroicons-information-circle text-info'
   */
  trailingOpen?: string
  /**
   * Update item leading icon when accordion button item is closed,
   * Accepts icon name and utility classes
   *
   * @example
   * trailingClose='i-heroicons-information-circle text-info'
   */
  trailingClose?: string
  /**
   * Allow accordion item to be open by default
   *
   * @default false
   */
  defaultOpen?: boolean
  /**
   * Close other accordion items when item is open
   *
   * @default false
   */
  closeOthers?: boolean
  /**
   * By default, all the accordion item is unmounted for performance reasons,
   * You can use the `mounted` prop to render the accordion specific on item.
   *
   * @default false
   */
  mounted?: boolean
  /**
   * Allow dynamic attributes to be added to the accordion item,
   *
   */
  [key: string]: any
}

Components

Accordion.vue
<script setup lang="ts">
import type { NAccordionItemProps, NAccordionProps } from '../../types'

import {
  Disclosure,
  DisclosureButton,
  DisclosurePanel,
} from '@headlessui/vue'
import { createReusableTemplate } from '@vueuse/core'
import { computed, ref } from 'vue'

import { pickProps } from '../../utils'
import NButton from './Button.vue'
import NIcon from './Icon.vue'

const props = withDefaults(defineProps<NAccordionProps>(), {
  trailingOpen: 'accordion-trailing-icon',
  loadingPlacement: 'trailing',
})

const buttonRefs = ref<(() => void)[]>([])

function closeOthers(index: number): void {
  if (props.multiple && !props.items[index].closeOthers)
    return

  buttonRefs.value
    .filter((_, i) => i !== index)
    .forEach(close => close())
}

function onEnter(element: Element, done: () => void): void {
  const el = element as HTMLElement
  el.style.height = '0'
  el.style.height = `${element.scrollHeight}px`
  el.addEventListener('transitionend', done, { once: true })
}

function onAfterEnter(element: Element): void {
  const el = element as HTMLElement
  el.style.height = 'auto'
}

function onBeforeLeave(element: Element): void {
  const el = element as HTMLElement
  el.style.height = `${el.scrollHeight}px`
}

function onLeave(element: Element, done: () => void): void {
  const el = element as HTMLElement
  el.style.height = '0'

  el.addEventListener('transitionend', done, { once: true })
}

const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
  item: NAccordionItemProps
  index: number
  open: boolean
  close: () => void
}>()

const pickedProps = pickProps(props, ['reverse', 'icon', 'btn', 'label', 'leading', 'loading', 'loadingPlacement', 'una', 'trailing', 'leading', 'to', 'type', 'disabled'])

function mergedProps(itemProps: NAccordionItemProps): NAccordionItemProps {
  return Object.assign(pickedProps, itemProps)
}

// TODO: refactor this to sync with NButton variants
const btnVariants = ['solid', 'outline', 'soft', 'ghost', 'link', 'text'] as const
const hasVariant = computed(() => btnVariants.some(btnVariants => props.btn?.includes(btnVariants)))
const isBaseVariant = computed(() => props.btn?.includes('~'))
</script>

<template>
  <div
    :accordion="accordion"
    class="accordion"
    :class="[
      unstyle ? 'space-y-3' : 'accordion-(border divider)',
      una?.accordion,
    ]"
  >
    <DefineTemplate v-slot="{ item, close, index, open }: any">
      <slot
        :name="item.content ? 'content' : index"
        :item="item" :index="index" :open="open" :close="close"
      >
        <div
          accordion="panel"
          :class="[
            una?.accordionPanel,
            { 'border-t-0': unstyle },
          ]"
        >
          {{ item.content }}
        </div>
      </slot>
    </DefineTemplate>

    <Disclosure
      v-for="(item, i) in items"
      :key="i"
      v-slot="{ open, close }"
      as="div"
      accordion="item"
      :default-open="item.defaultOpen || defaultOpen"
      :class="una?.accordionItem"
    >
      <DisclosureButton
        :ref="() => (buttonRefs[i] = close)"
        as="template"
        :disabled="item.disabled || disabled"
        @click="closeOthers(i)"
      >
        <slot name="label" :item="item" :index="i" :open="open" :close="close">
          <NButton
            v-bind="mergedProps(item)"
            :btn="`~ block ${btn || ''}`"
            :class="[
              { 'accordion-button-default-variant': !hasVariant && !isBaseVariant },
              { 'accordion-button-padding': !unstyle },
              una?.accordionButton,
            ]"
            :una="{
              btn: 'h-auto accordion-button',
              btnLabel: 'accordion-label',
            }"
          >
            <template #leading>
              <!-- TODO: fix conditional statement -->
              <NIcon
                v-if="leading || item.leading"
                accordion="leading"
                :class="una?.accordionLeading"
                :name="item.leading || leading || ''"
                aria-hidden="true"
              />
            </template>

            <template #trailing>
              <span
                v-if="trailingOpen || trailingClose"
                accordion="trailing"
                :class="[
                  trailingClose || (!trailingClose && open)
                    ? una?.accordionTrailingClose || 'accordion-trailing-close'
                    : una?.accordionTrailingOpen || 'accordion-trailing-open',
                  una?.accordionTrailing,
                ]"
              >
                <NIcon
                  v-if="(open || !trailingClose) && trailingOpen"
                  :name="trailingOpen"
                  aria-hidden="true"
                />
                <NIcon
                  v-else-if="!open && trailingClose"
                  :name="trailingClose"
                  aria-hidden="true"
                />
              </span>
            </template>
          </NButton>
        </slot>
      </DisclosureButton>

      <Transition
        :enter-active-class="una?.accordionLeaveActive || 'accordion-leave-active'"
        :leave-active-class="una?.accordionEnterActive || 'accordion-enter-active'"
        @enter="onEnter"
        @after-enter="onAfterEnter"
        @before-leave="onBeforeLeave"
        @leave="onLeave"
      >
        <DisclosurePanel v-if="!item.mounted || !mounted">
          <ReuseTemplate
            v-bind="{
              item,
              index: i,
              open,
              close,
            }"
          />
        </DisclosurePanel>
        <DisclosurePanel
          v-else
          v-show="open"
          static
        >
          <ReuseTemplate
            v-bind="{
              item,
              index: i,
              open,
              close,
            }"
          />
        </DisclosurePanel>
      </Transition>
    </Disclosure>
  </div>
</template>