Classic Accordion
A classic accordion component that expands and collapses content sections to organize information efficiently.
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; Installation
Run the command from your project root directory (the folder that contains package.json).
Terminal / Console
npx mosaicui-cli@latest essentials/classic-accordion 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(() => (
`${accordionGroupId}-header-${item.id}`
), [accordionGroupId, item.id]);
const accordionPanelId = useMemo(() => (
`${accordionGroupId}-panel-${item.id}`
), [accordionGroupId, item.id]);
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;
}
}
}
} Examples
Discover how supported props transform components to match your needs.
Open multiple accordions
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. |