Components
Accordion
Accordion
Ultra-modern accordion component with smooth animations, multiple variants, and customizable collapsible content sections.
Accordion Variants
Default Variant
What is FluxUI?
โ
How do I get started?
๐
Bordered Variant
What is FluxUI?
โ
How do I get started?
๐
Filled Variant
What is FluxUI?
โ
How do I get started?
๐
Minimal Variant
What is FluxUI?
โ
How do I get started?
๐
Accordion Sizes
Small Size
What is FluxUI?
โ
How do I get started?
๐
Medium Size (Default)
What is FluxUI?
โ
How do I get started?
๐
Large Size
What is FluxUI?
โ
How do I get started?
๐
Multiple Selection Mode
What is FluxUI?
โ
How do I get started?
๐
Is it free to use?
๐ฐ
Does it support dark mode?
๐
Accordion with Icons
Lightning Fast Performance
Fully Customizable
TypeScript Support
๐ท
Accessibility First
โฟ
Complex Content (Settings Panel)
Account Settings
Notification Preferences
Privacy & Security
FAQ Section
Frequently Asked Questions
Find answers to common questions about FluxUI
โ
What is FluxUI?
๐
How do I get started?
๐ฐ
Is it free to use?
๐
Does it support dark mode?
Product Features Showcase
Why Choose FluxUI?
Lightning Fast Performance
Fully Customizable
TypeScript Support
๐ท
Accessibility First
โฟ
Getting Started Guide
Installation
๐ฆ
Basic Setup
โ๏ธ
Customization
๐จ
Keyboard Navigation
Try these keyboard interactions:
- Tab: Move focus between accordion headers
- Enter/Space: Expand or collapse the focused item
- Arrow Up/Down: Navigate between accordion items
- Home/End: Jump to first/last item
What is FluxUI?
โ
How do I get started?
๐
Is it free to use?
๐ฐ
Custom Animation Duration
Fast Animation (200ms)
What is FluxUI?
โ
How do I get started?
๐
Slow Animation (600ms)
What is FluxUI?
โ
How do I get started?
๐
Installation
1
Install the packages
npm i motion clsx tailwind-merge lucide-react2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
accordion.tsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { ChevronDown, ChevronRight, Plus, Minus } from "lucide-react";
interface AccordionItem {
key: string;
title: string;
content: string | React.ReactNode;
icon?: string | React.ReactNode;
disabled?: boolean;
className?: string;
}
interface AccordionProps {
items: AccordionItem[];
variant?: "default" | "bordered" | "filled" | "minimal";
size?: "sm" | "md" | "lg";
allowMultiple?: boolean;
defaultOpen?: string | string[];
iconPosition?: "left" | "right";
animationDuration?: number;
disabled?: boolean;
onChange?: (openItems: string[]) => void;
className?: string;
}
const Accordion: React.FC<AccordionProps> = ({
items,
variant = "default",
size = "md",
allowMultiple = false,
defaultOpen,
iconPosition = "right",
animationDuration = 300,
disabled = false,
onChange,
className,
}) => {
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
// Initialize default open items
useEffect(() => {
if (defaultOpen) {
const defaultItems = Array.isArray(defaultOpen) ? defaultOpen : [defaultOpen];
setOpenItems(new Set(defaultItems));
}
}, [defaultOpen]);
// Handle item toggle
const handleToggle = (itemKey: string) => {
if (disabled) return;
const item = items.find(i => i.key === itemKey);
if (item?.disabled) return;
setOpenItems(prev => {
const newSet = new Set(prev);
if (allowMultiple) {
if (newSet.has(itemKey)) {
newSet.delete(itemKey);
} else {
newSet.add(itemKey);
}
} else {
if (newSet.has(itemKey)) {
newSet.clear();
} else {
newSet.clear();
newSet.add(itemKey);
}
}
const openArray = Array.from(newSet);
onChange?.(openArray);
return newSet;
});
};
// Size classes
const sizeClasses = {
sm: {
container: "text-sm",
header: "px-4 py-3",
content: "px-4 pb-3",
icon: "w-4 h-4",
},
md: {
container: "text-base",
header: "px-6 py-4",
content: "px-6 pb-4",
icon: "w-5 h-5",
},
lg: {
container: "text-lg",
header: "px-8 py-5",
content: "px-8 pb-5",
icon: "w-6 h-6",
},
};
// Variant classes
const variantClasses = {
default: {
container: "divide-y divide-gray-200 dark:divide-gray-700",
item: "bg-white dark:bg-gray-800",
header: "hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "bg-gray-50/50 dark:bg-gray-700/50",
},
bordered: {
container: "space-y-2",
item: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",
header: "hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-700/50",
},
filled: {
container: "space-y-2",
item: "bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden",
header: "hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "bg-white dark:bg-gray-900",
},
minimal: {
container: "divide-y divide-gray-100 dark:divide-gray-800",
item: "bg-transparent",
header: "hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors duration-200 rounded-lg",
content: "bg-transparent",
},
};
const currentSize = sizeClasses[size];
const currentVariant = variantClasses[variant];
return (
<div className={cn("w-full", currentSize.container, className)}>
<div className={currentVariant.container}>
{items.map((item, index) => {
const isOpen = openItems.has(item.key);
const isDisabled = disabled || item.disabled;
return (
<motion.div
key={item.key}
className={cn(currentVariant.item, item.className)}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
{/* Header */}
<motion.div
className={cn(
"flex items-center justify-between",
currentSize.header,
currentVariant.header,
isDisabled && "opacity-50 cursor-not-allowed"
)}
onClick={() => handleToggle(item.key)}
whileHover={!isDisabled ? { scale: 1.01 } : {}}
whileTap={!isDisabled ? { scale: 0.99 } : {}}
>
<div className="flex items-center gap-3 flex-1">
{/* Icon */}
{item.icon && iconPosition === "left" && (
<motion.div
className="flex-shrink-0"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
{typeof item.icon === 'string' ? (
<span className="text-lg">{item.icon}</span>
) : (
item.icon
)}
</motion.div>
)}
{/* Title */}
<motion.h3
className={cn(
"font-medium text-gray-900 dark:text-white flex-1",
isDisabled && "text-gray-400 dark:text-gray-500"
)}
animate={{
x: isOpen ? 4 : 0,
}}
transition={{ duration: 0.3 }}
>
{item.title}
</motion.h3>
{/* Icon */}
{item.icon && iconPosition === "right" && (
<motion.div
className="flex-shrink-0"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
{typeof item.icon === 'string' ? (
<span className="text-lg">{item.icon}</span>
) : (
item.icon
)}
</motion.div>
)}
</div>
{/* Expand/Collapse Icon */}
<motion.div
className="flex-shrink-0 ml-3"
animate={{
rotate: isOpen ? 180 : 0,
scale: isOpen ? 1.1 : 1,
}}
transition={{ duration: 0.3 }}
>
{variant === "minimal" ? (
isOpen ? (
<Minus className={cn(currentSize.icon, "text-gray-500")} />
) : (
<Plus className={cn(currentSize.icon, "text-gray-500")} />
)
) : (
<ChevronDown className={cn(currentSize.icon, "text-gray-500")} />
)}
</motion.div>
</motion.div>
{/* Content */}
<AnimatePresence>
{isOpen && (
<motion.div
className={cn(currentSize.content, currentVariant.content)}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: animationDuration / 1000,
ease: "easeInOut"
}}
>
<motion.div
className="pt-2"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{
duration: 0.2,
delay: 0.1
}}
>
{typeof item.content === 'string' ? (
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
{item.content}
</p>
) : (
item.content
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
</div>
);
};
export default Accordion;"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { ChevronDown, ChevronRight, Plus, Minus } from "lucide-react";
interface AccordionItem {
key: string;
title: string;
content: string | React.ReactNode;
icon?: string | React.ReactNode;
disabled?: boolean;
className?: string;
}
interface AccordionProps {
items: AccordionItem[];
variant?: "default" | "bordered" | "filled" | "minimal";
size?: "sm" | "md" | "lg";
allowMultiple?: boolean;
defaultOpen?: string | string[];
iconPosition?: "left" | "right";
animationDuration?: number;
disabled?: boolean;
onChange?: (openItems: string[]) => void;
className?: string;
}
const Accordion: React.FC<AccordionProps> = ({
items,
variant = "default",
size = "md",
allowMultiple = false,
defaultOpen,
iconPosition = "right",
animationDuration = 300,
disabled = false,
onChange,
className,
}) => {
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
// Initialize default open items
useEffect(() => {
if (defaultOpen) {
const defaultItems = Array.isArray(defaultOpen) ? defaultOpen : [defaultOpen];
setOpenItems(new Set(defaultItems));
}
}, [defaultOpen]);
// Handle item toggle
const handleToggle = (itemKey: string) => {
if (disabled) return;
const item = items.find(i => i.key === itemKey);
if (item?.disabled) return;
setOpenItems(prev => {
const newSet = new Set(prev);
if (allowMultiple) {
if (newSet.has(itemKey)) {
newSet.delete(itemKey);
} else {
newSet.add(itemKey);
}
} else {
if (newSet.has(itemKey)) {
newSet.clear();
} else {
newSet.clear();
newSet.add(itemKey);
}
}
const openArray = Array.from(newSet);
onChange?.(openArray);
return newSet;
});
};
// Size classes
const sizeClasses = {
sm: {
container: "text-sm",
header: "px-4 py-3",
content: "px-4 pb-3",
icon: "w-4 h-4",
},
md: {
container: "text-base",
header: "px-6 py-4",
content: "px-6 pb-4",
icon: "w-5 h-5",
},
lg: {
container: "text-lg",
header: "px-8 py-5",
content: "px-8 pb-5",
icon: "w-6 h-6",
},
};
// Variant classes
const variantClasses = {
default: {
container: "divide-y divide-gray-200 dark:divide-gray-700",
item: "bg-white dark:bg-gray-800",
header: "hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "bg-gray-50/50 dark:bg-gray-700/50",
},
bordered: {
container: "space-y-2",
item: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",
header: "hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-700/50",
},
filled: {
container: "space-y-2",
item: "bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden",
header: "hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200",
content: "bg-white dark:bg-gray-900",
},
minimal: {
container: "divide-y divide-gray-100 dark:divide-gray-800",
item: "bg-transparent",
header: "hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors duration-200 rounded-lg",
content: "bg-transparent",
},
};
const currentSize = sizeClasses[size];
const currentVariant = variantClasses[variant];
return (
<div className={cn("w-full", currentSize.container, className)}>
<div className={currentVariant.container}>
{items.map((item, index) => {
const isOpen = openItems.has(item.key);
const isDisabled = disabled || item.disabled;
return (
<motion.div
key={item.key}
className={cn(currentVariant.item, item.className)}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
{/* Header */}
<motion.div
className={cn(
"flex items-center justify-between",
currentSize.header,
currentVariant.header,
isDisabled && "opacity-50 cursor-not-allowed"
)}
onClick={() => handleToggle(item.key)}
whileHover={!isDisabled ? { scale: 1.01 } : {}}
whileTap={!isDisabled ? { scale: 0.99 } : {}}
>
<div className="flex items-center gap-3 flex-1">
{/* Icon */}
{item.icon && iconPosition === "left" && (
<motion.div
className="flex-shrink-0"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
{typeof item.icon === 'string' ? (
<span className="text-lg">{item.icon}</span>
) : (
item.icon
)}
</motion.div>
)}
{/* Title */}
<motion.h3
className={cn(
"font-medium text-gray-900 dark:text-white flex-1",
isDisabled && "text-gray-400 dark:text-gray-500"
)}
animate={{
x: isOpen ? 4 : 0,
}}
transition={{ duration: 0.3 }}
>
{item.title}
</motion.h3>
{/* Icon */}
{item.icon && iconPosition === "right" && (
<motion.div
className="flex-shrink-0"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
{typeof item.icon === 'string' ? (
<span className="text-lg">{item.icon}</span>
) : (
item.icon
)}
</motion.div>
)}
</div>
{/* Expand/Collapse Icon */}
<motion.div
className="flex-shrink-0 ml-3"
animate={{
rotate: isOpen ? 180 : 0,
scale: isOpen ? 1.1 : 1,
}}
transition={{ duration: 0.3 }}
>
{variant === "minimal" ? (
isOpen ? (
<Minus className={cn(currentSize.icon, "text-gray-500")} />
) : (
<Plus className={cn(currentSize.icon, "text-gray-500")} />
)
) : (
<ChevronDown className={cn(currentSize.icon, "text-gray-500")} />
)}
</motion.div>
</motion.div>
{/* Content */}
<AnimatePresence>
{isOpen && (
<motion.div
className={cn(currentSize.content, currentVariant.content)}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: animationDuration / 1000,
ease: "easeInOut"
}}
>
<motion.div
className="pt-2"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{
duration: 0.2,
delay: 0.1
}}
>
{typeof item.content === 'string' ? (
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
{item.content}
</p>
) : (
item.content
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
</div>
);
};
export default Accordion;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | AccordionItem[] | [] | Array of accordion items with title, content, and optional properties. |
| variant | 'default' | 'bordered' | 'filled' | 'minimal' | 'default' | Visual style variant of the accordion. |
| size | 'sm' | 'md' | 'lg' | 'md' | Size of the accordion items. |
| allowMultiple | boolean | false | Allow multiple accordion items to be open simultaneously. |
| defaultOpen | string | string[] | undefined | Default open accordion item(s) by key. |
| iconPosition | 'left' | 'right' | 'right' | Position of the expand/collapse icon. |
| animationDuration | number | 300 | Duration of the expand/collapse animation in milliseconds. |
| disabled | boolean | false | Disable all accordion interactions. |
| onChange | (openItems: string[]) => void | undefined | Callback function when accordion state changes. |