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>
)
}