UIPackage
Menu

Framework

Change language

Boilerplate repo

Accordion

accordion ui
Boilerplate repo

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 Radix UI with smooth animation and full keyboard support.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/accordion.json
Named registry: npx shadcn@latest add @uipkge-react/accordion Installs to: components/ui/accordion/

Examples

Loading interactive previews…

Props

Name Type / Values Default Required
variant default optional

Used by

Files installed (3)

  • components/ui/accordion/accordion.tsx 4.1 kB
    'use client'
    
    import * as React from 'react'
    import * as AccordionPrimitive from '@radix-ui/react-accordion'
    import { ChevronDown } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { accordionVariants, accordionItemVariants, accordionTriggerVariants } from './accordion.variants'
    
    type AccordionVariant = 'default' | 'separated' | 'ghost'
    
    // The Vue source threads the chosen variant from <Accordion> down to each
    // <AccordionItem>/<AccordionTrigger> via provide/inject. React's equivalent is
    // context — the Root sets it, Item/Trigger read it (default 'default').
    const AccordionVariantContext = React.createContext<AccordionVariant>('default')
    
    const Accordion = React.forwardRef<
      React.ElementRef<typeof AccordionPrimitive.Root>,
      React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root> & { variant?: AccordionVariant }
    >(({ className, variant = 'default', ...props }, ref) => (
      <AccordionVariantContext.Provider value={variant}>
        <AccordionPrimitive.Root
          ref={ref}
          data-uipkge=""
          data-slot="accordion"
          data-variant={variant}
          className={cn(accordionVariants({ variant }), className)}
          {...props}
        />
      </AccordionVariantContext.Provider>
    ))
    Accordion.displayName = 'Accordion'
    
    const AccordionItem = React.forwardRef<
      React.ElementRef<typeof AccordionPrimitive.Item>,
      React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
    >(({ className, ...props }, ref) => {
      const variant = React.useContext(AccordionVariantContext)
      return (
        <AccordionPrimitive.Item
          ref={ref}
          data-uipkge=""
          data-slot="accordion-item"
          className={cn(accordionItemVariants({ variant }), className)}
          {...props}
        />
      )
    })
    AccordionItem.displayName = 'AccordionItem'
    
    const AccordionHeader = React.forwardRef<
      React.ElementRef<typeof AccordionPrimitive.Header>,
      React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Header>
    >(({ className, ...props }, ref) => (
      <AccordionPrimitive.Header
        ref={ref}
        data-uipkge=""
        data-slot="accordion-header"
        className={cn('flex', className)}
        {...props}
      />
    ))
    AccordionHeader.displayName = 'AccordionHeader'
    
    const AccordionTrigger = React.forwardRef<
      React.ElementRef<typeof AccordionPrimitive.Trigger>,
      React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
    >(({ className, children, ...props }, ref) => {
      const variant = React.useContext(AccordionVariantContext)
      // Do not wrap Header here — matches Vue/Reka and lets consumers use
      // <AccordionHeader> for custom layouts (e.g. price outside the trigger).
      return (
        <AccordionPrimitive.Trigger
          ref={ref}
          data-uipkge=""
          data-slot="accordion-trigger"
          className={cn(accordionTriggerVariants({ variant }), className)}
          {...props}
        >
          {children}
          <ChevronDown
            className="text-muted-foreground size-4 shrink-0 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none group-data-[state=open]/accordion-trigger:rotate-180"
            aria-hidden="true"
          />
        </AccordionPrimitive.Trigger>
      )
    })
    AccordionTrigger.displayName = 'AccordionTrigger'
    
    const AccordionContent = React.forwardRef<
      React.ElementRef<typeof AccordionPrimitive.Content>,
      React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
    >(({ className, children, ...props }, ref) => (
      <AccordionPrimitive.Content
        ref={ref}
        data-uipkge=""
        data-slot="accordion-content"
        className={cn(
          'text-muted-foreground overflow-hidden text-sm',
          // Height via Radix CSS vars + tw-animate-css (not height:auto).
          // duration/ease set --tw-duration/--tw-ease consumed by the utility.
          'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
          'duration-200 ease-[cubic-bezier(0.32,0.72,0,1)]',
          'motion-reduce:animate-none',
          className,
        )}
        {...props}
      >
        <div className="pt-0 pb-4">{children}</div>
      </AccordionPrimitive.Content>
    ))
    AccordionContent.displayName = 'AccordionContent'
    
    export { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent }
  • 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>
  • components/ui/accordion/index.ts 0.4 kB
    export { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from './accordion'
    
    // Re-export variant API from the sibling file (kept separate to avoid an
    // import cycle and to give consumers a stable `accordionVariants` path).
    export {
      accordionVariants,
      accordionItemVariants,
      accordionTriggerVariants,
      type AccordionVariants,
      type AccordionItemVariants,
      type AccordionTriggerVariants,
    } from './accordion.variants'

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