Accordion

Accordion is a stacked list of expandable items that users can open and close to reveal or hide content. It helps organize information efficiently.

The accordion can be built using the following component,

  • AccordionItem – The container for each item / section.
  • AccordionHeader – The clickable element that toggles visibility.
  • AccordionPanel – The content area that expands or collapses.

Available from eds-core/1.20.0

Quick Start

Installation
npm install @adaptavant/eds-core
Import
import { Accordion } from '@adaptavant/eds-core';

Usage

Each AccordionItem has its own props to manage the header and content panel. By default, it behaves as an uncontrolled component as it manages its own internal state.

<Stack className="gap-3 w-full">
	<AccordionItem>
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel>
			Accordion is a component that allows you to show or hide content. 
			It is useful when you want to display a large amount of information 
			but don't want to overwhelm the user.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem>
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel>
			Yes, the Accordion component is accessible by default.
		</AccordionPanel>
	</AccordionItem>
</Stack>

Open By Default

Use isDefaultOpen prop in AccordionItem to have any item open by default.

Accordion is a component that allows you to show or hide content. It is useful when you want to display a large amount of information but don't want to overwhelm the user.

<Stack className="gap-3 w-full">
	<AccordionItem isDefaultOpen={true}>
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel>
			Accordion is a component that allows you to show or hide content. 
			It is useful when you want to display a large amount of information 
			but don't want to overwhelm the user.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem>
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel>
			Yes, the Accordion component is accessible by default. It also supports ARIA attributes and roles for improved accessibility.
		</AccordionPanel>
	</AccordionItem>
</Stack>

Divider

In case a visual separator is required between Accordion items, add the hasDivider prop for the AccordionItem.

<Stack className="gap-3 w-full">
	<AccordionItem hasDivider>
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel>
			Accordion is a component that allows you to show or hide content. 
			It is useful when you want to display a large amount of information 
			but don't want to overwhelm the user.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem hasDivider>
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel>
			Yes, the Accordion component is accessible by default. It also supports ARIA attributes and roles for improved accessibility.
		</AccordionPanel>
	</AccordionItem>
</Stack>

Controlled

If you want full control over the accordion behaviour, you can manage it manually using the id, isOpen and onToggle props.

  • Assign a unique id to each item.
  • Use the isOpen prop to control whether the item is expanded.
  • Use the onToggle callback to update the item's state when it's toggled.

const [openItemsId, setOpenItemsId] = React.useState([]);
const handleToggle = ({ id, isOpen }) => {
    setOpenItemsId((prev) => {
        if (prev) {
            return isOpen ? [...prev, id] : prev.filter((openId) => openId !== id);
        } else {
            return isOpen ? [id] : [];
        }
    });
};

return (
    <Stack className="gap-3 w-full">
        <AccordionItem
            id="item-1"
            isOpen={openItemsId?.includes('item-1')}
            onToggle={handleToggle}
        >
            <AccordionHeader>What is Accordion?</AccordionHeader>
            <AccordionPanel>
                Accordion is a component that allows you to show or hide content. It
                is useful when you want to display a large amount of information but
                don't want to overwhelm the user.
            </AccordionPanel>
        </AccordionItem>
        <AccordionItem
            id="item-2"
            isOpen={openItemsId?.includes('item-2')}
            onToggle={handleToggle}
        >
            <AccordionHeader>Is it accessible?</AccordionHeader>
            <AccordionPanel>
                Yes, the Accordion component is accessible by default.
            </AccordionPanel>
        </AccordionItem>
    </Stack>
);

Open one at a time

If you need only one accordion item to be open at a time, you can handle it by storing the currently open item's id (like openId) in your local state.

const [openId, setOpenId] = React.useState(""); // add item id, to have an accordion item open by default.

const handleToggle = (id) => {
    if (openId === id) {
        setOpenId(undefined);
    } else {
        setOpenId(id);
    }
};

return (
    <Stack className="gap-3 w-full">
        <AccordionItem
            id="item-1"
            isOpen={openId === 'item-1'}
            onToggle={({ id }) => {
                handleToggle(id);
            }}
        >
            <AccordionHeader>What is Accordion?</AccordionHeader>
            <AccordionPanel>
                Accordion is a component that allows you to show or hide content. It
                is useful when you want to display a large amount of information but
                don't want to overwhelm the user.
            </AccordionPanel>
        </AccordionItem>
        <AccordionItem
            id="item-2"
            isOpen={openId === 'item-2'}
            onToggle={({ id }) => {
                handleToggle(id);
            }}
        >
            <AccordionHeader>Is it accessible?</AccordionHeader>
            <AccordionPanel>
                Yes, the Accordion component is accessible by default.
            </AccordionPanel>
        </AccordionItem>
    </Stack>
);

Unmount Panel

Use unMountOnClose prop on AccordionPanel to completely remove the content from the DOM when it's not visible.

By default, it is set to false considering SEO and server-side rendering. Enable it only if you need to remove content from the DOM for performance.

<Stack className="gap-3 w-full">
	<AccordionItem id="item-1">
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel unMountOnClose={true}>
			Accordion is a component that allows you to show or hide content. 
			It is useful when you want to display a large amount of information 
			but don't want to overwhelm the user.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem id="item-2">
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel unMountOnClose={true}>
			Yes, the Accordion component is accessible by default. It also supports ARIA attributes and roles for improved accessibility.
		</AccordionPanel>
	</AccordionItem>
</Stack>

Custom example

To further customize the AccordionItem component, you can add classes to the styleable parts of the AccordionHeader and AccordionPanel using the classNames prop. In the following use case, we wanted to remove the default horizontal padding for the Accordion, so px-0 class is applied to the button and panelContent parts, and add divider for AccordionItem

Depending on your product's use case, you can add the divider, override spacing, alignment, or any other style in a similar way. For the list of styleable parts, refer the Style API section below.

Standard haircut with shampoo
1hr ·Details· $50
Scissors cut with shampoo
30min ·Details· $60
Zero fade with shampoo
45min ·Details· $55
A timeless gentlemans cut - short back and sides haircut, please DO NOT choose this cut if would like any type of fade as the time allocation is different

    
const servicesData = [
    {
        serviceName: 'Standard haircut with shampoo',
        duration: '1hr',
        cost: '$50'
    },
    {
        serviceName: 'Scissors cut with shampoo',
        duration: '30min',
        cost: '$60'
    },
    {
        serviceName: 'Zero fade with shampoo',
        duration: '45min',
        cost: '$55',
        description: 'A timeless gentlemans cut - short back and sides haircut, please DO NOT choose this cut if would like any type of fade as the time allocation is different'
    },
];

const ServiceSlot = ({ name, duration, cost, description }) => (
    <Track
        verticalAlign="top"
        railStart={
            <Avatar name="Freddy" size="40">
                <AvatarIcon icon={ProfileIcon} />
            </Avatar>
        }
        railEnd={
            <IconButton
            aria-label="More details"
            icon={ChevronRightIcon}
            size="standard"
            variant="neutralTertiary"
            />
        }
        className="gap-3"
    >
        <Stack>
            <Text className="text-body-12">{name}</Text>
            <Box className="inline-flex items-center gap-2 text-body-12">
                <Text className="text-body-12">{duration} &middot;</Text>
                <TextLink>Details</TextLink>
                <Text className="text-body-12">&middot; {cost}</Text>
            </Box>
            { description ? <Text className="text-body-12 text-secondary pt-2">{description}</Text> : null }
        </Stack>
    </Track>
);
return (
    <Stack className="gap-3 w-full">
        <AccordionItem isDefaultOpen hasDivider>
            <AccordionHeader classNames={{ button: 'px-0' }}>Haircut services</AccordionHeader>
            <AccordionPanel classNames={{ panelContent: 'px-0' }}>
                <Box className="flex flex-col gap-8">
                    {servicesData.map((service) => <ServiceSlot key={service.serviceName} name={service.serviceName} duration={service.duration} cost={service.cost} description={service.description} />
                    )}
                </Box>
            </AccordionPanel>
        </AccordionItem>
        <AccordionItem hasDivider>
            <AccordionHeader classNames={{ button: 'px-0' }}>Salon services</AccordionHeader>
            <AccordionPanel classNames={{ panelContent: 'px-0' }}>
                <Box className="flex flex-col gap-8">
                    <ServiceSlot name="Facial with detan" duration="30min" cost="$30" />
                </Box>
            </AccordionPanel>
        </AccordionItem>
        <AccordionItem hasDivider>
            <AccordionHeader classNames={{ button: 'px-0' }}>Other services</AccordionHeader>
            <AccordionPanel classNames={{ panelContent: 'px-0' }}>
                <Box className="flex flex-col gap-8">
                    <ServiceSlot name="Book a consultation" duration="30min" cost="$10" />
                </Box>
            </AccordionPanel>
        </AccordionItem>
    </Stack>
);
      

API Reference

AccordionHeader

PropDefaultDescription
children_React.ReactNode
The content of the accordion header.
as?h3'h2' | 'h3' | 'h4' | 'h5' | 'h6'
The HTML heading level / tag to use for the header
onClick?_function
Callback function that is called when the header is clicked.
buttonRef?_React.RefObject<HTMLButtonElement>
Used to set the ref of the button element.

AccordionItem

PropDefaultDescription
children_React.ReactNode
The content of the accordion item.
id?_string
The unique id of the accordion item. This id should be used to set the open/closed state of each accordion item.
isDefaultOpen?falseboolean
Used to set the default open state of the item when used as uncontrolled component.
isOpen?_boolean
Used to set the open state of the item when used as controlled component.
onToggle?_({ isOpen, id } : { isOpen: boolean, id: string }) => void
Callback function that is called when the item is toggled. isOpen is a boolean that indicates the open state of the item. id is the unique id of the item.
hasDivider?falseboolean
Adds a bottom border below the accordion item.

AccordionPanel

PropDefaultDescription
children_React.ReactNode
The content of the accordion panel.
unMountOnClose?falseboolean
Used to remove the content when the item is closed.

Style API

Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.

Please refer to the Style API documentation for more insights.

AccordionItem parts

<React.Fragment>
	<AccordionItem className="border border-input-critical" id="item-1">
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel>
			Accordion is a component that allows you to show or hide content.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem className="border border-input-critical" id="item-2">
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel>
			Yes, the Accordion component is accessible by default.
		</AccordionPanel>
	</AccordionItem>
</React.Fragment>

No parts available. Only root.

AccordionHeader parts

<React.Fragment>
	<AccordionItem id="item-1">
		<AccordionHeader
			className="bg-accent-secondary p-1"
			classNames={{
				button: 'border border-input-critical',
				buttonLabel: 'text-positive p-1 border',
				iconEnd: 'fill-critical',
			}}
		>
			What is Accordion?
		</AccordionHeader>
		<AccordionPanel>
			Accordion is a component that allows you to show or hide content.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem id="item-2">
		<AccordionHeader
			className="bg-accent-secondary p-1"
			classNames={{
				button: 'border border-input-critical',
				buttonLabel: 'text-positive p-1 border',
				iconEnd: 'fill-critical',
			}}
		>
			Is it accessible?
		</AccordionHeader>
		<AccordionPanel>
			Yes, the Accordion component is accessible by default.
		</AccordionPanel>
	</AccordionItem>
</React.Fragment>
Stylable PartsDescription
buttonThe clickable element that toggles the accordion panel.
buttonLabelThe text content of the accordion header.
iconEndThe icon displayed at the end.

AccordionPanel parts

Accordion is a component that allows you to show or hide content.

<React.Fragment>
	<AccordionItem id="item-1" isDefaultOpen={true}>
		<AccordionHeader>What is Accordion?</AccordionHeader>
		<AccordionPanel classNames={{ panelContent: 'bg-accent-secondary-pressed' }}>
			Accordion is a component that allows you to show or hide content.
		</AccordionPanel>
	</AccordionItem>
	<AccordionItem id="item-2">
		<AccordionHeader>Is it accessible?</AccordionHeader>
		<AccordionPanel classNames={{ panelContent: 'bg-accent-secondary-pressed' }}>
			Yes, the Accordion component is accessible by default.
		</AccordionPanel>
	</AccordionItem>
</React.Fragment>
Stylable PartsDescription
panelContentThe text content of the accordion panel.