Classic Accordion
A classic accordion component that expands and collapses content sections to organize information efficiently.
Our pricing is based on usage and plan tier.
You can upgrade, downgrade, or cancel at any time from your account settings.
Yes. We offer a 14-day free trial with full access to all features.
No credit card is required to get started.
We provide email support on all plans.
Priority and live chat support are available on higher-tier plans.
We use industry-standard encryption in transit and at rest.
Access to customer data is restricted and monitored.
1. Copy the component file
Create a new file called classic-accordion.jsx in
your reusable components folder (for example
/src/components/) and paste the following code
into it.
classic-accordion.jsx
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import styles from "./classic-accordion.module.css";
const AccordionItem = (props) => {
const {
accordionGroupId,
item,
onToggle,
open,
} = props;
const panelRef = useRef(null);
const accordionHeaderId = useMemo((id) => (
`${accordionGroupId}-header-${id}`
), [accordionGroupId]);
const accordionPanelId = useMemo((id) => (
`${accordionGroupId}-panel-${id}`
), [accordionGroupId]);
useLayoutEffect(() => {
const height = open ? panelRef.current.scrollHeight : 0;
panelRef.current?.style.setProperty("--panel-height", `${height}px`);
}, [open]);
return (
<div className={styles["accordion-item"]}>
<div className={styles["header"]}>
<button
aria-controls={accordionPanelId}
aria-expanded={open}
id={accordionHeaderId}
onClick={() => onToggle(item.id)}
>
<span
className={styles["title"]}
title={item.title}
>
{item.title}
</span>
<span className={styles["icon"]}>
</span>
</button>
</div>
<div
aria-labelledby={accordionHeaderId}
className={styles["panel"]}
id={accordionPanelId}
ref={panelRef}
role="region"
>
<div className={styles["panel-content-wrapper"]}>
{item.content}
</div>
</div>
</div>
);
};
const ClassicAccordion = (props) => {
const {
items = [],
multiple = false,
defaultOpen,
} = props;
const [openAccordionIds, setOpenAccordionIds] = useState(() => {
if (multiple) {
if(Array.isArray(defaultOpen)) {
return [...defaultOpen];
}
} else if(
typeof defaultOpen === "string" &&
items.some(item => item.id === defaultOpen)
) {
return [defaultOpen];
}
return [];
});
const accordionGroupId = useMemo(() => (
`accordion-group-${Math.random().toString(36).substring(2)}`
), []);
const handleAccordionToggle = useCallback((accordionId) => {
setOpenAccordionIds((prev) => {
const isOpen = prev.includes(accordionId);
if (isOpen) {
return prev.filter(item => item !== accordionId);
} else if (multiple) {
return [...prev, accordionId];
} else {
return [accordionId];
}
});
}, []);
if(
!Array.isArray(items) ||
items.length === 0
) {
return null;
}
return (
<div className={styles["accordion-group"]}>
{items.map((item, index) => (
<AccordionItem
key={item.id ?? index}
accordionGroupId={accordionGroupId}
item={item}
open={openAccordionIds.includes(item.id)}
onToggle={handleAccordionToggle}
/>
))}
</div>
);
};
export default memo(ClassicAccordion); 2. Copy the CSS module file
In the same folder, create a file called classic-accordion.module.css and paste the following CSS.
classic-accordion.module.css
.accordion-group {
--accordion-border-color: #bbb;
--accordion-header-text-color: #111;
--accordion-padding: 16px;
--accordion_open-background-color: #eee;
--transition-duration: 250ms;
@media (prefers-color-scheme: dark) {
--accordion-header-text-color: #f5f5f5;
--accordion-border-color: #222;
--accordion_open-background-color: #111;
}
border: 1px solid var(--accordion-border-color);
border-radius: 16px;
overflow: hidden;
>.accordion-item {
border-bottom: 1px solid var(--accordion-border-color);
&:last-child {
border-bottom: none;
}
>.header {
button {
padding: var(--accordion-padding);
width: 100%;
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
background: transparent;
border: none;
font: inherit;
cursor: pointer;
.title {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--accordion-header-text-color);
}
.icon {
position: relative;
width: 12px;
height: 12px;
flex-shrink: 0;
&:before,
&:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 1px;
background: var(--accordion-header-text-color);
}
&:before {
transform: translate(-50%, -50%) rotate(90deg);
}
}
}
}
>.panel {
--panel-height: 0px;
overflow: hidden;
height: var(--panel-height) !important;
border-top: 1px solid transparent;
transition:
height var(--transition-duration) ease-in-out,
border 0ms linear var(--transition-duration);
.panel-content-wrapper {
padding: var(--accordion-padding);
}
}
&:has([aria-expanded="true"]) {
>.header {
background: var(--accordion_open-background-color);
>button {
.icon {
&:before {
display: none;
}
}
}
}
>.panel {
border-top-color: var(--accordion-border-color);
transition:
height var(--transition-duration) ease-in-out,
border 0ms linear 0ms;
}
}
}
} 3. Use the component
Now you can import and use the component anywhere in your project.
classic-accordion-preview.jsx
import ClassicAccordion from "@/components/essentials/classic-accordion/classic-accordion";
const ClassicAccordionPreview = () => {
return (
<div style={{
width: "100%",
maxWidth: "480px",
padding: "16px",
}}>
<ClassicAccordion
items={faq}
/>
</div>
)
};
const faq = [
{
id: "pricing",
title: "How does pricing work?",
content: `Our pricing is based on usage and plan tier.
You can upgrade, downgrade, or cancel at any time from your account settings.`,
},
{
id: "trial",
title: "Do you offer a free trial?",
content: `Yes. We offer a 14-day free trial with full access to all features.
No credit card is required to get started.`,
},
{
id: "support",
title: "What kind of support do you provide?",
content: `We provide email support on all plans.
Priority and live chat support are available on higher-tier plans.`,
},
{
id: "security",
title: "How is my data secured?",
content: `We use industry-standard encryption in transit and at rest.
Access to customer data is restricted and monitored.`,
},
];
export default ClassicAccordionPreview; Open multiple accordions
Our pricing is based on usage and plan tier.
You can upgrade, downgrade, or cancel at any time from your account settings.
Yes. We offer a 14-day free trial with full access to all features.
No credit card is required to get started.
We provide email support on all plans.
Priority and live chat support are available on higher-tier plans.
We use industry-standard encryption in transit and at rest.
Access to customer data is restricted and monitored.
classic-accordion
import ClassicAccordion from "@/components/essentials/classic-accordion/classic-accordion";
const ClassicAccordionMultiplePreview = () => {
return (
<div style={{
width: "100%",
maxWidth: "480px",
padding: "16px",
}}>
<ClassicAccordion
items={faq}
multiple
/>
</div>
)
};
const faq = [
{
id: "pricing",
title: "How does pricing work?",
content: `Our pricing is based on usage and plan tier.
You can upgrade, downgrade, or cancel at any time from your account settings.`,
},
{
id: "trial",
title: "Do you offer a free trial?",
content: `Yes. We offer a 14-day free trial with full access to all features.
No credit card is required to get started.`,
},
{
id: "support",
title: "What kind of support do you provide?",
content: `We provide email support on all plans.
Priority and live chat support are available on higher-tier plans.`,
},
{
id: "security",
title: "How is my data secured?",
content: `We use industry-standard encryption in transit and at rest.
Access to customer data is restricted and monitored.`,
},
];
export default ClassicAccordionMultiplePreview; Props
Configure the component with the following props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| items | { id: string; title: React.ReactNode; content: React.ReactNode }[] | Yes | [] | Array of accordion items to render. |
| multiple | boolean | No | false | Allows multiple panels to be open at the same time. |
| defaultOpen | string | string[] | No | — | ID (or array of IDs when multiple is true) of item(s) open by default. |