DocsAccordion

Accordion (FAQ section)

Smooth, SEO friendly and accessible FAQ section that leaves no questions

Berserk. It's about a warrior born from a dead flesh of a hanged mother, marked by the brand of sacrifice by apostles of the devil themselves. But he never gave a fuck and fought them as if he were immortal, despite the unending enemies and absence of hope for escape. Through his endless struggle, he became a creator of his own destiny and escaped unavoidable death at the Eclipse.
stop watching anime, hit the gym, go to japan
Dude named Luka Donadze (@lukachodonadze)

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion @tabler/icons-react

Create @/utils/cn.ts file

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

create accordion.tsx file

'use client'

import { cn } from '@/utils/cn'

import { Dispatch, SetStateAction, createContext, useContext, useState } from 'react'
import { motion } from 'framer-motion'
import { IconChevronDown } from '@tabler/icons-react'
import { twMerge } from 'tailwind-merge'

export const Accordion: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className
}) => {
  return <dl className={cn('flex flex-col items-start justify-start', className)}>{children}</dl>
}

const TabContext = createContext<{
  isOpen: boolean
  setOpenState: Dispatch<SetStateAction<boolean>>
} | null>(null)

export const Tab: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className
}) => {
  const [isOpen, setOpenState] = useState(false)

  return (
    <TabContext.Provider value={{ isOpen, setOpenState }}>
      <div className={cn('bg-bg w-full p-6', className)}>{children}</div>
    </TabContext.Provider>
  )
}

export const Trigger: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className
}) => {
  const { setOpenState, isOpen } = useContext(TabContext)!

  return (
    <dt>
      <button
        aria-expanded={isOpen}
        onClick={() => setOpenState((e) => !e)}
        className={cn(
          'flex w-full items-center justify-between gap-2 text-start text-xl font-normal',
          className
        )}>
        <span>{children}</span>
        <IconChevronDown
          size={'20'}
          className={twMerge(
            isOpen ? 'rotate-180' : 'rotate-0',
            'min-w-[20px] transition-all duration-300'
          )}
        />
      </button>
    </dt>
  )
}

export const Content: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className
}) => {
  const { isOpen } = useContext(TabContext)!
  return (
    <motion.dd
      layout
      aria-hidden={isOpen}
      className={cn('overflow-hidden text-secondary', className)}
      initial={{ height: 0, pointerEvents: 'none' }}
      animate={
        isOpen
          ? { height: 'fit-content', pointerEvents: 'auto', marginTop: '1rem' }
          : { height: 0, pointerEvents: 'none' }
      }
      transition={{ duration: 0.2 }}>
      {children}
    </motion.dd>
  )
}