Accordion
A vertically stacked set of interactive headings that each reveal an associated section of content.
- Full keyboard navigation
- Can be controlled or uncontrolled
- Fully animated transitions
- Can be used as a single or multiple accordion
Installation
Install the following dependencies
npm install clsx @radix-ui/react-accordion
Copy and paste the following code into your project
primitives/accordion/index.tsx
'use client';import * as AccordionPrimitive from '@radix-ui/react-accordion';import { clsx } from 'clsx';import { ComponentPropsWithoutRef, useEffect, useState } from 'react';export interface AccordionProps extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> { colorScheme?: 'light' | 'dark';}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { * --accordion-focus: hsl(var(--primary)); * --acordion-light-offset: hsl(var(--background)); * --accordion-light-title-text: hsl(var(--contrast-400)); * --accordion-light-title-text-hover: hsl(var(--foreground)); * --accordion-light-title-icon: hsl(var(--contrast-500)); * --accordion-light-title-icon-hover: hsl(var(--foreground)); * --accordion-light-content-text: hsl(var(--foreground)); * --acordion-dark-offset: hsl(var(--foreground)); * --accordion-dark-title-text: hsl(var(--contrast-200)); * --accordion-dark-title-text-hover: hsl(var(--background)); * --accordion-dark-title-icon: hsl(var(--contrast-200)); * --accordion-dark-title-icon-hover: hsl(var(--background)); * --accordion-dark-content-text: hsl(var(--background)); * --accordion-title-font-family: var(--font-family-mono); * --accordion-content-font-family: var(--font-family-body); * } * ``` */function AccordionItem({ title, children, colorScheme = 'light', className, ...props}: AccordionProps) { const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); return ( <AccordionPrimitive.Item {...props} className={clsx( 'focus:outline-2 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[var(--accordion-focus,hsl(var(--primary)))] has-[:focus-visible]:ring-offset-4', { light: 'ring-offset-[var(--acordion-light-offset,hsl(var(--background)))]', dark: 'ring-offset-[var(--acordion-dark-offset,hsl(var(--foreground)))]', }[colorScheme], className, )} > <AccordionPrimitive.Header> <AccordionPrimitive.Trigger className="group flex w-full cursor-pointer items-start gap-8 border-none py-3 text-start focus:outline-none @md:py-4"> <div className={clsx( 'flex-1 select-none font-[family-name:var(--accordion-title-font-family,var(--font-family-mono))] text-sm font-normal uppercase transition-colors duration-300 ease-out', { light: 'text-[var(--accordion-light-title-text,hsl(var(--contrast-400)))] group-hover:text-[var(--accordion-light-title-text-hover,hsl(var(--foreground)))]', dark: 'text-[var(--accordion-dark-title-text,hsl(var(--contrast-200)))] group-hover:text-[var(--accordion-dark-title-text-hover,hsl(var(--background)))]', }[colorScheme], )} > {title} </div> <AnimatedChevron className={clsx( { light: 'stroke-[var(--accordion-light-title-icon,hsl(var(--contrast-500)))] group-hover:stroke-[var(--accordion-light-title-icon-hover,hsl(var(--foreground)))]', dark: 'stroke-[var(--accordion-dark-title-icon,hsl(var(--contrast-200)))] group-hover:stroke-[var(--accordion-dark-title-icon-hover,hsl(var(--background)))]', }[colorScheme], )} /> </AccordionPrimitive.Trigger> </AccordionPrimitive.Header> <AccordionPrimitive.Content className={clsx( 'overflow-hidden', // We need to delay the animation until the component is mounted to avoid the animation // from being triggered when the component is first rendered. isMounted && 'data-[state=closed]:animate-collapse data-[state=open]:animate-expand', )} > <div className={clsx( 'py-3 font-[family-name:var(--accordion-content-font-family,var(--font-family-body))] text-base font-light leading-normal', { light: 'text-[var(--accordion-light-content-text,hsl(var(--foreground)))]', dark: 'text-[var(--accordion-dark-content-text,hsl(var(--background)))]', }[colorScheme], )} > {children} </div> </AccordionPrimitive.Content> </AccordionPrimitive.Item> );}function AnimatedChevron({ className, ...props}: React.JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) { return ( <svg {...props} className={clsx( 'mt-1 shrink-0 [&>line]:origin-center [&>line]:transition [&>line]:duration-300 [&>line]:ease-out', className, )} viewBox="0 0 10 10" width={16} > {/* Left Line of Chevron */} <line className="group-data-[state=open]:-translate-y-[3px] group-data-[state=open]:-rotate-90" strokeLinecap="round" x1={2} x2={5} y1={2} y2={5} /> {/* Right Line of Chevron */} <line className="group-data-[state=open]:-translate-y-[3px] group-data-[state=open]:rotate-90" strokeLinecap="round" x1={8} x2={5} y1={2} y2={5} /> </svg> );}const Accordion = AccordionPrimitive.Root;export { Accordion, AccordionItem };
Usage
import { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion';const accordionItems = [ { title: 'What is your return policy?', content: 'Our return policy allows you to return items within 30 days of purchase for a full refund. Items must be in their original condition and packaging.', }, { title: 'How long does shipping take?', content: 'Shipping typically takes 3-5 business days for domestic orders. International shipping may take longer depending on the destination.', },];function Usage() { return ( <Accordion type="multiple"> {accordionItems.map(({ title, content }, index) => ( <AccordionItem key={index} title={title} value={index.toString()}> {content} </AccordionItem> ))} </Accordion> );}
API Reference
AccordionProps
Refer to the Radix UI documentation for the Accordion.Root component.
AccordionItem
Prop | Type | Default |
---|---|---|
children* | ReactNode | |
className | string | |
colorScheme | 'dark' | 'light' | 'light' |
title* | string |
Refer to the Radix UI documentation for the Accordion.Item for additional properties.
CSS Variables
This component supports various CSS variables for theming. Here's a comprehensive list.
:root { --accordion-focus: hsl(var(--primary)); --acordion-light-offset: hsl(var(--background)); --accordion-light-title-text: hsl(var(--contrast-400)); --accordion-light-title-text-hover: hsl(var(--foreground)); --accordion-light-title-icon: hsl(var(--contrast-500)); --accordion-light-title-icon-hover: hsl(var(--foreground)); --accordion-light-content-text: hsl(var(--foreground)); --acordion-dark-offset: hsl(var(--foreground)); --accordion-dark-title-text: hsl(var(--contrast-200)); --accordion-dark-title-text-hover: hsl(var(--background)); --accordion-dark-title-icon: hsl(var(--contrast-200)); --accordion-dark-title-icon-hover: hsl(var(--background)); --accordion-dark-content-text: hsl(var(--background)); --accordion-title-font-family: var(--font-family-mono); --accordion-content-font-family: var(--font-family-body);}