Collapsible content React components
React makes collapsible content components a breeze with its state management and component composition. Simply put, you get full control over the content and its visibility, allowing you to create collapses, accordions, tabs and carousels.
Collapsible content
For a simple collapsible content component, you need the useState()
hook to manage the state of the collapsible content. Then, using a <button>
to toggle the visibility of the content, you can make the content collapse or expand by changing the state.
For accessibility purposes, it's a good idea to use the aria-expanded
attribute to indicate the state of the collapsible content.
.collapse-button { display: block; width: 100%; } .collapse-content.collapsed { display: none; } .collapsed-content.expanded { display: block; }
const Collapse = ({ collapsed, children }) => { const [isCollapsed, setIsCollapsed] = React.useState(collapsed); return ( <> <button className="collapse-button" onClick={() => setIsCollapsed(!isCollapsed)} > {isCollapsed ? 'Show' : 'Hide'} content </button> <div className={`collapse-content ${isCollapsed ? 'collapsed' : 'expanded'}`} aria-expanded={isCollapsed} > {children} </div> </> ); }; ReactDOM.createRoot(document.getElementById('root')).render( <Collapse> <h1>This is a collapse</h1> <p>Hello world!</p> </Collapse> );
Collapsible accordion
Accordions are very similar, but we need to manage the state a little differently, holding the index of the active accordion item. We can then toggle the visibility of the content based on the active index.
In order to make the component easier to read, we'll extract the AccordionItem
component to handle each accordion item. The Accordion
component will manage the state and render the items.
As an additional feature, we can pass a callback function to the Accordion
component to handle the click event on each accordion item. We may also want to filter out any non-AccordionItem
children.
.accordion-item.collapsed { display: none; } .accordion-item.expanded { display: block; } .accordion-button { display: block; width: 100%; }
const AccordionItem = ({ label, isCollapsed, handleClick, children }) => { return ( <> <button className="accordion-button" onClick={handleClick}> {label} </button> <div className={`accordion-item ${isCollapsed ? 'collapsed' : 'expanded'}`} aria-expanded={isCollapsed} > {children} </div> </> ); }; const Accordion = ({ defaultIndex, onItemClick, children }) => { const [bindIndex, setBindIndex] = React.useState(defaultIndex); const changeItem = itemIndex => { if (typeof onItemClick === 'function') onItemClick(itemIndex); if (itemIndex !== bindIndex) setBindIndex(itemIndex); }; const items = children.filter(item => item.type.name === 'AccordionItem'); return ( <> {items.map(({ props }) => ( <AccordionItem isCollapsed={bindIndex !== props.index} label={props.label} handleClick={() => changeItem(props.index)} children={props.children} /> ))} </> ); }; ReactDOM.createRoot(document.getElementById('root')).render( <Accordion defaultIndex="1" onItemClick={console.log}> <AccordionItem label="A" index="1"> Lorem ipsum </AccordionItem> <AccordionItem label="B" index="2"> Dolor sit amet </AccordionItem> </Accordion> );
Tabs
Tabs are virtually identical to an accordion, but with a different visual representation. The Tabs
component will manage the state and render the tab items. Again, we'll extract the TabItem
component to handle each tab item.
.tab-menu > button { cursor: pointer; padding: 8px 16px; border: 0; border-bottom: 2px solid transparent; background: none; } .tab-menu > button.focus { border-bottom: 2px solid #007bef; } .tab-menu > button:hover { border-bottom: 2px solid #007bef; } .tab-content { display: none; } .tab-content.selected { display: block; }
const TabItem = props => <div {...props} />; const Tabs = ({ defaultIndex = 0, onTabClick, children }) => { const [bindIndex, setBindIndex] = React.useState(defaultIndex); const changeTab = newIndex => { if (typeof onTabClick === 'function') onTabClick(newIndex); setBindIndex(newIndex); }; const items = children.filter(item => item.type.name === 'TabItem'); return ( <div className="wrapper"> <div className="tab-menu"> {items.map(({ props: { index, label } }) => ( <button key={`tab-btn-${index}`} onClick={() => changeTab(index)} className={bindIndex === index ? 'focus' : ''} > {label} </button> ))} </div> <div className="tab-view"> {items.map(({ props }) => ( <div {...props} className={`tab-content ${ bindIndex === props.index ? 'selected' : '' }`} key={`tab-content-${props.index}`} /> ))} </div> </div> ); }; ReactDOM.createRoot(document.getElementById('root')).render( <Tabs defaultIndex="1" onTabClick={console.log}> <TabItem label="A" index="1"> Lorem ipsum </TabItem> <TabItem label="B" index="2"> Dolor sit amet </TabItem> </Tabs> );
Carousel
Finally, a carousel is a type of collapsible content that automatically scrolls through items. We can use the useEffect()
hook to update the active item index at regular intervals, with the help of setTimeout()
.
.carousel { position: relative; } .carousel-item { position: absolute; visibility: hidden; } .carousel-item.visible { visibility: visible; }
const Carousel = ({ carouselItems, ...rest }) => { const [active, setActive] = React.useState(0); let scrollInterval = null; React.useEffect(() => { scrollInterval = setTimeout(() => { setActive((active + 1) % carouselItems.length); }, 2000); return () => clearTimeout(scrollInterval); }); return ( <div className="carousel"> {carouselItems.map((item, index) => { const activeClass = active === index ? ' visible' : ''; return React.cloneElement(item, { ...rest, className: `carousel-item${activeClass}` }); })} </div> ); }; ReactDOM.createRoot(document.getElementById('root')).render( <Carousel carouselItems={[ <div>carousel item 1</div>, <div>carousel item 2</div>, <div>carousel item 3</div> ]} /> );