UIPackage

Accordion

Vue disclosure
Edit on GitHub

Vertically stacked, collapsible panels — one or many open at a time. Use for FAQs, settings groups, and any place where space is tight but content needs to stay browsable. Built on reka-ui with smooth animation and full keyboard support.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/accordion.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/accordion

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional
variant
'default''separated''ghost'
'default' optional
type
'single''multiple'
optional
modelValue string | string[] optional
defaultValue string | string[] optional
collapsible boolean optional
disabled boolean optional
dir
'ltr''rtl'
optional
orientation
'horizontal''vertical'
optional
asChild boolean optional
as string | object optional

Dependencies

Used by

Files (7)

  • app/components/ui/accordion/Accordion.vue 1.4 kB
    <script setup lang="ts">
    import { computed, provide } from 'vue'
    import type { AccordionRootEmits } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { AccordionRoot, useForwardPropsEmits } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import { accordionVariants } from './accordion.variants'
    
    // Inlined unions: SFC compiler can't extract runtime props from
    // `NonNullable<AccordionVariants['variant']>` or reka-ui's
    // `AccordionRootProps`. Inline only the surface we expose.
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        variant?: 'default' | 'separated' | 'ghost'
        type?: 'single' | 'multiple'
        modelValue?: string | string[]
        defaultValue?: string | string[]
        collapsible?: boolean
        disabled?: boolean
        dir?: 'ltr' | 'rtl'
        orientation?: 'horizontal' | 'vertical'
        asChild?: boolean
        as?: string | object
      }>(),
      {
        variant: 'default',
      },
    )
    
    const emits = defineEmits<AccordionRootEmits>()
    
    const delegated = reactiveOmit(props, 'class', 'variant')
    const forwarded = useForwardPropsEmits(delegated, emits)
    
    provide(
      Symbol.for('accordionVariant'),
      computed(() => props.variant),
    )
    </script>
    
    <template>
      <AccordionRoot
        data-uipkge
        data-slot="accordion"
        :data-variant="variant"
        v-bind="forwarded"
        :class="cn(accordionVariants({ variant }), props.class)"
      >
        <slot />
      </AccordionRoot>
    </template>
  • app/components/ui/accordion/AccordionContent.vue 0.8 kB
    <script setup lang="ts">
    import type { AccordionContentProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { AccordionContent as RkAccordionContent } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
    const delegated = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <RkAccordionContent
        data-uipkge
        data-slot="accordion-content"
        v-bind="delegated"
        :class="
          cn(
            'text-muted-foreground overflow-hidden text-sm',
            'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
            props.class,
          )
        "
      >
        <div class="pt-0 pb-4">
          <slot />
        </div>
      </RkAccordionContent>
    </template>
  • app/components/ui/accordion/AccordionHeader.vue 0.6 kB
    <script setup lang="ts">
    import type { AccordionHeaderProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { AccordionHeader as RkAccordionHeader } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<AccordionHeaderProps & { class?: HTMLAttributes['class'] }>()
    const delegated = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <RkAccordionHeader data-uipkge data-slot="accordion-header" v-bind="delegated" :class="cn('flex', props.class)">
        <slot />
      </RkAccordionHeader>
    </template>
  • app/components/ui/accordion/AccordionItem.vue 0.9 kB
    <script setup lang="ts">
    import { computed, inject } from 'vue'
    import type { AccordionItemProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { AccordionItem as RkAccordionItem } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import { accordionItemVariants } from './accordion.variants'
    
    const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
    const delegated = reactiveOmit(props, 'class')
    
    const variantRef = inject<{ value: 'default' | 'separated' | 'ghost' } | undefined>(
      Symbol.for('accordionVariant'),
      undefined,
    )
    const variant = computed(() => variantRef?.value ?? 'default')
    </script>
    
    <template>
      <RkAccordionItem
        data-uipkge
        data-slot="accordion-item"
        v-bind="delegated"
        :class="cn(accordionItemVariants({ variant }), props.class)"
      >
        <slot />
      </RkAccordionItem>
    </template>
  • app/components/ui/accordion/AccordionTrigger.vue 1.1 kB
    <script setup lang="ts">
    import { computed, inject } from 'vue'
    import type { AccordionTriggerProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { AccordionTrigger as RkAccordionTrigger } from 'reka-ui'
    import { ChevronDown } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { accordionTriggerVariants } from './accordion.variants'
    
    const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
    const delegated = reactiveOmit(props, 'class')
    
    const variantRef = inject<{ value: 'default' | 'separated' | 'ghost' } | undefined>(
      Symbol.for('accordionVariant'),
      undefined,
    )
    const variant = computed(() => variantRef?.value ?? 'default')
    </script>
    
    <template>
      <RkAccordionTrigger
        data-uipkge
        data-slot="accordion-trigger"
        v-bind="delegated"
        :class="cn(accordionTriggerVariants({ variant }), props.class)"
      >
        <slot />
        <ChevronDown
          class="text-muted-foreground size-4 shrink-0 transition-transform duration-200 group-data-[state=open]/accordion-trigger:rotate-180"
          aria-hidden="true"
        />
      </RkAccordionTrigger>
    </template>
  • app/components/ui/accordion/accordion.variants.ts 1.8 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts`) so consuming Vue SFCs can import without creating a circular
     * dependency through the index. See card.variants.ts for the canonical
     * example + the SSR symptom that motivated the split.
     */
    
    export const accordionVariants = cva('w-full', {
      variants: {
        variant: {
          // Bottom-border between items (the classic shadcn look).
          default: '',
          // Each item is its own bordered card with a small gap between them.
          separated: 'space-y-2',
          // Borderless. Use when the parent container already provides framing.
          ghost: '',
        },
      },
      defaultVariants: {
        variant: 'default',
      },
    })
    
    export const accordionItemVariants = cva('', {
      variants: {
        variant: {
          default: 'border-b border-border last:border-b-0',
          separated: 'rounded-md border border-border bg-card overflow-hidden',
          ghost: '',
        },
      },
      defaultVariants: {
        variant: 'default',
      },
    })
    
    export const accordionTriggerVariants = cva(
      'group/accordion-trigger flex w-full items-center justify-between gap-3 text-sm font-medium text-foreground outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 hover:text-primary disabled:pointer-events-none disabled:opacity-50',
      {
        variants: {
          variant: {
            default: 'py-4',
            separated: 'px-4 py-3 hover:bg-muted/50',
            ghost: 'py-3',
          },
        },
        defaultVariants: {
          variant: 'default',
        },
      },
    )
    
    export type AccordionVariants = VariantProps<typeof accordionVariants>
    
    export type AccordionItemVariants = VariantProps<typeof accordionItemVariants>
    
    export type AccordionTriggerVariants = VariantProps<typeof accordionTriggerVariants>
  • app/components/ui/accordion/index.ts 0.7 kB
    export { default as Accordion } from './Accordion.vue'
    export { default as AccordionContent } from './AccordionContent.vue'
    export { default as AccordionHeader } from './AccordionHeader.vue'
    export { default as AccordionItem } from './AccordionItem.vue'
    export { default as AccordionTrigger } from './AccordionTrigger.vue'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // Component.vue <-> index.ts circular import that broke dev SSR for Card).
    export {
      accordionVariants,
      accordionItemVariants,
      accordionTriggerVariants,
      type AccordionVariants,
      type AccordionItemVariants,
      type AccordionTriggerVariants,
    } from './accordion.variants'

Raw manifest: https://uipkge.dev/r/vue/accordion.json