Make Accordion component based on this:
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as React from "react";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "@radix-ui/react-icons";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b border-border", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-left font-semibold transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon
width={16}
height={16}
strokeWidth={2}
className="shrink-0 opacity-60 transition-transform duration-200"
aria-hidden="true"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
but use styles and animation from this
// components/ui/faq-section.tsx
"use client";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown, Mail } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
interface FaqSectionProps extends React.HTMLAttributes<HTMLElement> {
title: string;
description?: string;
items: {
question: string;
answer: string;
}[];
contactInfo?: {
title: string;
description: string;
buttonText: string;
onContact?: () => void;
};
}
const FaqSection = React.forwardRef<HTMLElement, FaqSectionProps>(
({ className, title, description, items, contactInfo, ...props }, ref) => {
return (
<section
ref={ref}
className={cn(
"py-16 w-full bg-gradient-to-b from-transparent via-muted/50 to-transparent",
className
)}
{...props}
>
<div className="container">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="max-w-2xl mx-auto text-center mb-12"
>
<h2 className="text-3xl font-semibold mb-3 bg-gradient-to-r from-foreground via-foreground/80 to-foreground bg-clip-text text-transparent">
{title}
</h2>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</motion.div>
{/* FAQ Items */}
<div className="max-w-2xl mx-auto space-y-2">
{items.map((item, index) => (
<FaqItem
key={index}
question={item.question}
answer={item.answer}
index={index}
/>
))}
</div>
{/* Contact Section */}
{contactInfo && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="max-w-md mx-auto mt-12 p-6 rounded-lg text-center"
>
<div className="inline-flex items-center justify-center p-1.5 rounded-full mb-4">
<Mail className="h-4 w-4" />
</div>
<p className="text-sm font-medium text-foreground mb-1">
{contactInfo.title}
</p>
<p className="text-xs text-muted-foreground mb-4">
{contactInfo.description}
</p>
<Button size="sm" onClick={contactInfo.onContact}>
{contactInfo.buttonText}
</Button>
</motion.div>
)}
</div>
</section>
);
}
);
FaqSection.displayName = "FaqSection";
// Internal FaqItem component
const FaqItem = React.forwardRef<
HTMLDivElement,
{
question: string;
answer: string;
index: number;
}
>((props, ref) => {
const [isOpen, setIsOpen] = React.useState(false);
const { question, answer, index } = props;
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.1 }}
className={cn(
"group rounded-lg",
"transition-all duration-200 ease-in-out",
"border border-border/50",
isOpen
? "bg-gradient-to-br from-background via-muted/50 to-background"
: "hover:bg-muted/50"
)}
>
<Button
variant="ghost"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-6 py-4 h-auto justify-between hover:bg-transparent"
>
<h3
className={cn(
"text-base font-medium transition-colors duration-200 text-left",
"text-foreground/70",
isOpen && "text-foreground"
)}
>
{question}
</h3>
<motion.div
animate={{
rotate: isOpen ? 180 : 0,
scale: isOpen ? 1.1 : 1,
}}
transition={{ duration: 0.2 }}
className={cn(
"p-0.5 rounded-full flex-shrink-0",
"transition-colors duration-200",
isOpen ? "text-primary" : "text-muted-foreground"
)}
>
<ChevronDown className="h-4 w-4" />
</motion.div>
</Button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: "auto",
opacity: 1,
transition: { duration: 0.2, ease: "easeOut" },
}}
exit={{
height: 0,
opacity: 0,
transition: { duration: 0.2, ease: "easeIn" },
}}
>
<div className="px-6 pb-4 pt-2">
<motion.p
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
className="text-sm text-muted-foreground leading-relaxed"
>
{answer}
</motion.p>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
});
FaqItem.displayName = "FaqItem";
export { FaqSection };
Okay, I can help you create an Accordion
component that combines the structure of the Radix UI-based accordion with the styling and animation aesthetics from your FaqSection
example.
We'll update the AccordionItem
, AccordionTrigger
, and AccordionContent
components to reflect the visual style of the FaqItem
. The animations for opening and closing the accordion content will be handled by Tailwind CSS keyframes, mimicking the height
and opacity
transitions seen in the FaqSection
's Framer Motion setup. The chevron icon will also be updated and animated using CSS transitions.
Here's the implementation:
Key changes and features:
Styling:
AccordionItem
: Now has rounded corners, a full border, and a gradient background when open, with a hover effect when closed, similar to FaqItem
.AccordionTrigger
: Styled to match the FaqItem
's trigger, including text size, font weight, and color changes based on state. The chevron icon is from lucide-react
.AccordionContent
: Padding and text styling inside the content area match the FaqItem
.Icon:
ChevronDown
from lucide-react
.Animation:
AccordionContent
uses new Tailwind CSS keyframe animations (accordion-down
, accordion-up
) for a smooth height and opacity transition. These are defined in tailwind.config.ts
.ease-in
for closing and ease-out
for opening match the FaqItem
's animation timing.tailwind.config.ts
:
tailwind.config.ts
file to include the keyframes
and animation
definitions for accordion-down
and accordion-up
as shown. I've included a more complete tailwind.config.ts
example; you should merge the extend
section carefully with your existing configuration.Example Usage (app/page.tsx
):
FaqSection
example you provided.This setup provides an accordion component that is structurally sound (thanks to Radix UI) and visually aligned with the modern aesthetics of your FaqSection
example, using CSS for animations to keep it lightweight and performant.
Replace icons from the arrow to the plus and minus as we have in the second accordion component
Okay, I'll update the AccordionTrigger
component to use a Plus
icon when the item is closed and a Minus
icon when it's open, similar to the behavior you might see in other accordion implementations.
Here's how we can modify the components/ui/accordion.tsx
file:
Explanation of Changes:
Plus
and Minus
from lucide-react
in addition to ChevronDown
(though ChevronDown
is no longer used in the trigger, it might be used elsewhere or you can remove it if not needed).Conditional Rendering:
div
that holds the icon, we now have both <Plus />
and <Minus />
icons.Their visibility is controlled by Tailwind's data-[state=open]:opacity-0
and data-[state=closed]:opacity-0
classes.
Plus
icon is visible when data-state
is closed
(opacity 100) and hidden when open
(opacity 0).Minus
icon is visible when data-state
is open
(opacity 100) and hidden when closed
(opacity 0).Icon Styling:
data-[state=open]:rotate-180 data-[state=open]:scale-110
) are removed from the icon itself, as the icon swap handles the visual change.absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
to both icons and relative w-4 h-4
to their container div
to ensure they are perfectly centered and overlap correctly during the transition. This creates a smoother visual effect as one fades out and the other fades in.transition-opacity duration-200
class is added to both icons for a smooth fade effect.With these changes, the accordion trigger will now display a Plus
icon when closed and a Minus
icon when open, providing a clear visual cue for the item's state.
it should be minus when the item is open