All components
NucleusAccordion
NavigationNewAn animated accordion with an expanding chevron, spring height transitions and single or multiple open modes.
Preview
No. MIT licensed means the code is yours once you copy it. No attribution needed, no license headers to keep.
Installation
- 1shadcn/ui initialized (`npx shadcn@latest init`)
- 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-merge framer-motionAdd to your globals.css
The nucleusUI accent lives in these CSS variables. Without them the component falls back to the default shadcn zinc theme.
app/globals.css
:root {
--primary: oklch(0.54 0.24 291);
--primary-foreground: oklch(0.985 0 0);
--ring: oklch(0.54 0.24 291);
--accent: oklch(0.96 0.03 291);
--accent-foreground: oklch(0.4 0.19 291);
}
.dark {
--primary: oklch(0.68 0.21 291);
--primary-foreground: oklch(0.145 0 0);
--ring: oklch(0.68 0.21 291);
--accent: oklch(0.26 0.06 291);
--accent-foreground: oklch(0.87 0.09 291);
}Usage
tsx
import { NucleusAccordion } from "@/components/nucleus/nucleus-accordion";
const items = [
{ value: "one", title: "First question", content: <p>The answer.</p> },
{ value: "two", title: "Second question", content: <p>Another answer.</p> },
];
export function Example() {
return <NucleusAccordion items={items} multiple defaultValue="one" />;
}Source
Copy this file into components/nucleus/nucleus-accordion.tsx in your project, it's yours to own and modify.
components/nucleus/nucleus-accordion.tsx
"use client";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
export interface NucleusAccordionItemData {
/** Unique value used to identify the open item. */
value: string;
/** Row title. */
title: string;
/** Collapsed content. */
content: React.ReactNode;
}
export interface NucleusAccordionProps {
items: NucleusAccordionItemData[];
/** Allow more than one item open at once. Default true. */
multiple?: boolean;
/** Value opened on first render. */
defaultValue?: string;
className?: string;
}
/**
* Simple animated accordion. Items expand with spring physics and the
* chevron rotates in place. Keyboard accessible with native buttons.
*/
export function NucleusAccordion({
items,
multiple = true,
defaultValue,
className,
}: NucleusAccordionProps) {
const [open, setOpen] = React.useState<string[]>(
defaultValue ? [defaultValue] : []
);
const toggle = (value: string) => {
setOpen((prev) => {
const isOpen = prev.includes(value);
if (multiple) {
return isOpen ? prev.filter((v) => v !== value) : [...prev, value];
}
return isOpen ? [] : [value];
});
};
return (
<div
data-slot="nucleus-accordion"
className={cn("flex w-full flex-col", className)}
>
{items.map((item) => {
const isOpen = open.includes(item.value);
return (
<div key={item.value} className="border-b border-border/70 last:border-b-0">
<button
type="button"
aria-expanded={isOpen}
aria-controls={`panel-${item.value}`}
onClick={() => toggle(item.value)}
className="flex w-full cursor-pointer items-center justify-between gap-4 py-4 text-left text-sm font-medium transition-colors hover:text-primary"
>
{item.title}
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/70 text-muted-foreground"
>
<ChevronDown className="size-3.5" />
</motion.span>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
id={`panel-${item.value}`}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
className="overflow-hidden"
>
<div className="pb-4 text-sm leading-relaxed text-muted-foreground">
{item.content}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | { value: string; title: string; content: ReactNode }[] | — | Accordion rows: id, title and collapsible content. |
multiple | boolean | true | Allow more than one item open at once. |
defaultValue | string | — | Item value opened on first render. |
className | string | — | Extra classes for the root element. |