Accordion
A commonly used interface component is an Accordion.
The user sees a list of titles with an icon opposite the title text. Clicking on the title smoothly reveals additional content directly below the title. The icon rotates to indicate the open/closed state of the control.
An example accordion control written in Hyperscript is shown below.
Example
-
HTMX extends html with a suite of attributes giving any element the ability to send and respond to AJAX request.
-
Hyperscript is a front-end scripting language which leverages robust event handling to bring dynamic user interfaces to the browser.
Hyperscript syntax is based on Dan Winkler’s HyperTalk language made famous by Apple’s HyperCard program. The syntax boasts an English-prose like grammar which eases the learning curve for new programmers.
-
The Tailwind CSS library provides a rich ecosystem of utility classes that simplify responsive design by using prefixes to replace media queries and emphasizing mobile first design.
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Commodi, unde nostrum itaque aut explicabo deserunt vel sunt alias? Atque, a eveniet ad rerum labore deleniti tempora nisi commodi officiis quis eius perferendis.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis consectetur illum quis ipsam quos quidem eligendi ex blanditiis nihil temporibus veniam adipisci deleniti modi, facilis amet fugiat incidunt atque neque aut qui quia reprehenderit magni fugit aliquam. Hic dolorem consectetur qui libero eius commodi natus, quod fuga tenetur, non doloremque unde at impedit vero. Beatae quas excepturi dignissimos est assumenda quidem ullam quisquam labore! Quaerat veritatis ex sint similique veniam quidem numquam doloremque officiis et accusantium! Non nesciunt ea itaque explicabo ipsa blanditiis iusto, est deserunt inventore voluptas dolorem possimus at exercitationem aliquid, distinctio similique quae! Perferendis ipsam excepturi repudiandae.
-
Astro is a modern Vite-based build tool meant for speed which ships zero JavaScript by default. Initially released with support for static site generation, now Astro delivers a full suite of server rendered features.
Description
Accordions conserve screen real estate while still allowing considerable content to be displayed when needed.
To implement this control you need:
- A header (with a clickable title)
- A body (with hidden content that can be revealed)
In the collapsed state, the height of the body is set to 0. In response to a click event in the header, the body smoothly transitions to an open state by setting a final height and a transition animation. From a programming standpoint, the key is calculating the body’s final height based on the proposed content.
If too much content is revealed then a huge vertical shift occurs on the page which is distracting. Thus it is best to set a maxHeight after which extra content simply scrolls vertically.
The source code to create the example accordion is shown next with highlights indicating the Astro used. The complete source code for each component is presented in the Code section.
----import AccordionGroup_A from '../../../../components/AccordionGroup_A.astro'import AccordionItem_A from '../../../../components/AccordionItem_A.astro'
const tw - { p: 'pb-3'}---
<AccordionGroup_A closeSiblings={true} styles='mx-4'>
<AccordionItem_A id='accordion-htmx' caption="HTMX" > <p class={tw.p}> HTMX extends html with a suite of attributes giving any element the ability to send and respond to AJAX request. </p> </AccordionItem_A>
<AccordionItem_A id='accordion-hyperscript' caption='Hyperscript'> <p class={tw.p}> Hyperscript is a front-end scripting language which leverages robust event handling to bring dynamic user interfaces to the browser. </p> <p class={tw.p}> Hyperscript syntax is based on Dan Winkler's HyperTalk language made famous by Apple's HyperCard program. The syntax boasts an English-prose like grammar which eases the learning curve for new programmers. </p> </AccordionItem_A>
<AccordionItem_A id='accordion-tailwind' caption='Tailwind CSS'> <p class={tw.p}> The Tailwind CSS library provides a rich ecosystem of utility classes that simplify responsive design by using prefixes to replace media queries and emphasizing mobile first design. </p> <p class={tw.p}> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Commodi, unde nostrum itaque aut explicabo deserunt vel sunt alias? Atque, a eveniet ad rerum labore deleniti tempora nisi commodi officiis quis eius perferendis. </p> <p class={tw.p}> Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis consectetur illum quis ipsam quos quidem eligendi ex blanditiis nihil temporibus veniam adipisci deleniti modi, facilis amet fugiat incidunt atque neque aut qui quia reprehenderit magni fugit aliquam. Hic dolorem consectetur qui libero eius commodi natus, quod fuga tenetur, non doloremque unde at impedit vero. Beatae quas excepturi dignissimos est assumenda quidem ullam quisquam labore! Quaerat veritatis ex sint similique veniam quidem numquam doloremque officiis et accusantium! Non nesciunt ea itaque explicabo ipsa blanditiis iusto, est deserunt inventore voluptas dolorem possimus at exercitationem aliquid, distinctio similique quae! Perferendis ipsam excepturi repudiandae. </p> </AccordionItem_A>
<AccordionItem_A id='accordion-astro' caption='Astro'> <p class={tw.p}> Astro is a modern Vite-based build tool meant for speed which ships zero JavaScript by default. Initially released with support for static site generation, now Astro delivers a full suite of server rendered features. </p> </AccordionItem_A>
</AccordionGroup_A>Features
- Clicking the header toggles the body open or closed.
- An indicator icon rotates 180 degrees to indicate open/closed state
- Body content is revealed smoothly using a Hyperscript
transitionon the body’s *height. - A maxHeight prop prevents the body height from exceeding a set value.
- Tailwind classes for the header, body, and active header (when opened) can be passed in through props to customize the appearance of the entire control from outside.
- Content for the body is simplhy placed into the default slot of the AccordionItem_A component and thus can be styled to your liking from where the accordion is used.
Components
This control is comprised of two Astro components:
AccordionGroup_Awhich functions as a wrapper for all the individual items, andAccordionItem_Awhich consists of a button for the header and a div for the body content.
All of the source code, including the scripts and styling, is found in the Code section below.
Props
Props are used to configure both components from where they are used. This strategy allows full customization without having to rewrite the source code itself.
All props accept strings unless otherwise specified.
AccordionGroup_A
interface Props { closeSiblings?:boolean, styles?:string}
const { closeSiblings = false, styles} = Astro.propscloseSiblings
You can designate if opening an accordion should cause all other accordions to close. When passing true to the closeSiblings prop a custom closeOpenAccordions event is fired. All accordions except the one sending this event will be then closed. This was done in the example code above.
The default value for closeSiblings is false.
styles
This prop takes a string containing any Tailwind classes of your choosing. These classes will be merged with the default styles for AccordionGroup_A (which you find in the Code section below.
TW classes passed in the styles prop only affect the whole group and thus should be mostly positioning values such as width, margins, etc.
AccordionItem_A
interface Props { id:string, caption:string, prefix?:string maxHeight:number headerStyles?:string bodyStyles?:string activeHeaderStyles?:string openDuration?:string closeDuration?:string}const { id, caption, prefix='- ', maxHeight=300, headerStyles, bodyStyles, activeHeaderStyles = 'bg-orange-100 dark:bg-blue-950', openDuration='260ms', closeDuration='400ms'} = Astro.propsid
Each item should have a unique id to prevent naming collisions with other items.
caption
The caption is the text that appears in the accordion header.
prefix
This props accepts is a character or short string that is prepended to the caption. It acts as a tick mark. The default is -
maxHeight
This prop takes a number representing the maximum height (in pixels) of the accordion body. The default is 300.
The HS code checks if the calculated final height of the body is more than the value specified in the maxHeight prop.
If so, the body height is limited to the maxHeight value and a vertical scroll bar is shown.
headerStyles
This prop takes a string containing any Tailwind classes of your choosing. These classes will be merged with the default styles for the <button> element in the AccordionItem_A (which you find in the Code section below.
Use this prop to add or override styling of the accordion header.
bodyStyles
This prop takes a string containing any Tailwind classes of your choosing. These classes will be merged with the default styles for the <div> element of AccordionItem_A (which you find in the Code section below.
Use this prop to add or override styling of the accordion body.
activeHeaderStyles
This prop takes a string containing any Tailwind classes of your choosing. These classes will be added to the <button> element when the accordion is open.
Use this prop to add styling to the header only when it is open.
openDuration
This prop takes a string which sets the duration of the transition when the accordion body is shown. The default is '260ms'.
closeDuration
This prop takes a string which sets the duration of the transition when the accordion body is hidden. The default is '400ms'.
Styling
Both the AccordionGroup_A and AccordionItem_A components can be styled with suitable TW utility classes from outside of the component code base using props. This allows the appearance of both the group and individual components to be customized without having to modify the source code.
You can override the default styling by sending additional Tailwind classes via the above props which are then merged with the default styles and any conflicts resolved with a ‘last wins’ rule.
To improve readability, the Tailwind classes and Hyperscripts are both extracted from the markup and encapsulated into Astro tw and hs local variables within the Component Script which are then referenced in the html markup.
The advantage of this strategy is to allow long run-on strings to be formatted as concatenated, multi-line strings which aids readability and code maintenance.
Transitions
For this accordion, the transition command of Hyperscript is utilized to specify the CSS properties, duration, and timing function used to smoothly reveal the accordion body.
What makes this Hyperscript example unique is insertion of props into the script.
To make component props available to the script, the HS code is placed into a back-tick delimited JS string and stored in a local constant in the Astro Component Script (i.e. frontmatter). Dynamic values can thus be passed into the script itself with simple string interpolation creating another level of functionality.
Here is a brief code snippet of from AccordionItem_A where the maxHeight and openDuration component props are relayed into the script itself using string interpolation. Using this technique is analagous to sending parameters to the script from an outside source.
This ‘trick’ allows you to change the transition duration and other actions of the Hyperscript from outside of the component simply by sending new values to the script code via props.
`transition the #{:accordionBody}'s *height to ${maxHeight} using 'all ${openDuration} ease-in' `The local hs constant storing the HS code is then referenced in the markup as would any variable in the component frontmatter using curly braces as shown below.
<li class={tw.wrapper}>
<!-- Accordion Header --> <button id={id} class={tw.accordionHeader} script={hs}> <span>{prefix}{caption}</span> <!-- TwirlIcon --> <svg id={`${id}-twirl-icon`} class={tw.icon} fill='currentColor' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'> <path fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'></path> </svg> </button>
<!-- Accordion Body --> <div id={`${id}-body`} class={tw.accordionBody}> <slot /> </div>
</li>The syntax for the transition command of Hyperscript must be exact or it will not work. Following the using clause you must provide a quoted string analogous to how a transition in vanilla CSS would be written. If you leave out the quotes, the transition will fail silently.
See the Code section below for all the scripts and how they are populated with the values of the component props using this technique.
Slots
Both components use a default <slot> to receive child elements.
The AccordionGroup_A default <slot> is populated with accordion items.
The AccordionItem_A default <slot> is populated with whatever markup you wish to place into the accordion body.
Code
AccordionGroup_A
This example accordion is comprised of a wrapper component (AccordionGroup_A) which contains a default slot into which multiple accordion items (AccordionItem_A) are placed.
A minimal set of default styles are provided but can be overridden or added to by passing additional TW classes to the styles prop.
Explore the Tabs below to review all the code and styling which makes up the accordion group wrapper.
Copy/paste the contents of the Everything tab to use this component in your projects.
interface Props { closeSiblings?:boolean, //all other Accordions are sent a closeOpenAccordions event styles?:string //override or add TW classes for stying the Group}
const { closeSiblings = false, styles} = Astro.props<ul class={tw.group} script={hs}> <slot /></ul>const hs = `on closeOpenAccordions if ${closeSiblings} is true set allAccordions to the <li/> in me set allHeaders to the <button/> in allAccordions for header in allHeaders if header is not sender send closeAccordion to header end -- for end -- if closeSiblings end -- closeOpenAccordions`const tw = { group: twMerge('mx-auto', styles),}---import {twMerge} from 'tailwind-merge'
interface Props { closeSiblings?:boolean, //all other Accordions aresent a closeOpenAccordions event styles?:string //override or add TW classes for stying the Group}
const { closeSiblings = false, styles} = Astro.props
const tw = { group: twMerge('mx-auto', styles),}
const hs = `on closeOpenAccordions if ${closeSiblings} is true set allAccordions to the <li/> in me set allHeaders to the <button/> in allAccordions for header in allHeaders if header is not sender send closeAccordion to header end -- for end -- if closeSiblings end -- closeOpenAccordions`
---
<ul class={tw.group} script={hs}> <slot /></ul>AccordionItem_A
Most of the functionality of this control resides in the styling and actions of the AccordionItem_A component.
Each group should receive multiple AccordionItem_A components
Each AccordionItem_A consists of a <li> with a trigger <button> for the accordion header and a <div> which presents a default <slot> for the actual content.
Explore the Tabs below to review all the code and styling for the accordion item component.
Copy/paste the contents of the Everything tab to use this component in your projects.
interface Props { id:string, //a unique identifier allowing multiple AccordionItems on a page without collisions caption:string, //the text display in the Accordion header prefix?:string //a tick mark preceeding the caption maxHeight:number //maximum open height in px (after which the body content scrolls vertically) headerStyles?:string //override or add TW classes to the header bodyStyles?:string //override or add TW classes to the body activeHeaderStyles?:string //override, add, remove TW classes for headers of opened Accordions openDuration?:string //transition timing when showing body content closeDuration?:string //transition timing when hiding body content}const { id, caption, prefix='- ', maxHeight=300, headerStyles, bodyStyles, activeHeaderStyles = 'bg-orange-100 dark:bg-blue-800', openDuration='260ms', closeDuration='400ms'} = Astro.props<li class={tw.wrapper}>
<!-- Accordion Header --> <button id={id} class={tw.accordionHeader} script={hs}> <span>{prefix}{caption}</span> <!-- TwirlIcon --> <svg id={`${id}-twirl-icon`} class={tw.icon} fill='currentColor' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'> <path fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'></path> </svg> </button>
<!-- Accordion Body --> <div id={`${id}-body`} class={tw.accordionBody}> <slot /> </div>
</li>const hs = ` init set :accordionHeight to 0 set :accordionOpen to false set :accordionBody to my @id + '-body' set :twirlIcon to my @id + '-twirl-icon' set accordionContent to the children of the #{:accordionBody} for item in accordionContent increment :accordionHeight by the item's offsetHeight end -- for increment :accordionHeight by 45 -- to accomodate for py-4 end -- init
on click from me if :accordionOpen is true then trigger closeAccordion on me otherwise trigger openAccordion on me end end -- click
on openAccordion add ${activeStyles} to me add .py-4 .border-t-2 to the #{:accordionBody} if the :accordionHeight is greater than ${maxHeight} transition the #{:accordionBody}'s *height to ${maxHeight} px using 'all ${openDuration} ease-in' add .overflow-y-auto to the #{:accordionBody} otherwise transition the #{:accordionBody} *height to :accordionHeight px using 'all ${openDuration} ease-in' end -- if > maxHeight add .rotate-180 to the #{:twirlIcon} set :accordionOpen to true send closeOpenAccordions to the closest <ul/> end -- openAccordion
on closeAccordion remove ${activeStyles} from me remove .py-4 .border-t-2 from the #{:accordionBody} remove .rotate-180 from the #{:twirlIcon} transition the #{:accordionBody} *height to 0 using 'all ${closeDuration} ease-out' set :accordionOpen to false end -- closeAccordion`const tw = { wrapper:'shadow-lg overflow-hidden ' + 'border-4 border-b-0 last-of-type:border-b-4 border-indigo-500 border-opacity-20 dark:border-gray-700 ' + 'first-of-type:rounded-t-xl last-of-type:rounded-b-xl', accordionHeader: twMerge('w-full flex justify-between items-center px-2 py-5 select-none cursor-pointer ' + 'font-bold text-lg text-left text-gray-900 dark:text-zinc-300 ' + 'bg-blue-100 hover:bg-sky-950 hover:text-yellow-50 ' + 'dark:bg-slate-900 dark:hover:bg-purple-950 dark:hover:text-fuchsia-100 ' + '', headerStyles), accordionBody: twMerge('h-0 px-6 text-left border-zinc-300 ' + ' bg-gray-100 text-slate-800 dark:bg-violet-50 dark:text-gray-900', bodyStyles), icon: 'size-6 shrink-0'}---import {twMerge} from 'tailwind-merge'
interface Props { id:string, //a unique identifier allowing multiple AccordionItems on a page without collisions caption:string, //the text display in the Accordion header prefix?:string //a tick mark preceeding the caption maxHeight:number //maximum open height in px (after which the body content scrolls vertically) headerStyles?:string //override or add TW classes to the header bodyStyles?:string //override or add TW classes to the body activeHeaderStyles?:string //override, add, remove TW classes for headers of opened Accordions openDuration?:string //transition timing when showing body content closeDuration?:string //transition timing when hiding body content}const { id, caption, prefix='- ', maxHeight=300, headerStyles, bodyStyles, activeHeaderStyles = 'bg-orange-100 dark:bg-blue-800', openDuration='260ms', closeDuration='400ms'} = Astro.props
const tw = { wrapper:'shadow-lg overflow-hidden ' + 'border-4 border-b-0 last-of-type:border-b-4 border-indigo-500 border-opacity-20 dark:border-gray-700 ' + 'first-of-type:rounded-t-xl last-of-type:rounded-b-xl', accordionHeader: twMerge('w-full flex justify-between items-center px-2 py-5 select-none cursor-pointer ' + 'font-bold text-lg text-left text-gray-900 dark:text-zinc-300 ' + 'bg-blue-100 hover:bg-sky-950 hover:text-yellow-50 ' + 'dark:bg-slate-900 dark:hover:bg-purple-950 dark:hover:text-fuchsia-100 ' + '', headerStyles), accordionBody: twMerge('h-0 px-6 text-left border-zinc-300 ' + ' bg-gray-100 text-slate-800 dark:bg-violet-50 dark:text-gray-900', bodyStyles), icon: 'size-6 shrink-0'}
function formatTWClasses(twClasses) { return twClasses.split(' ').map(twClass => '.' + twClass).join(' ');}
const activeStyles = formatTWClasses(activeHeaderStyles)
const hs = ` init set :accordionHeight to 0 set :accordionOpen to false set :accordionBody to my @id + '-body' set :twirlIcon to my @id + '-twirl-icon' set accordionContent to the children of the #{:accordionBody} for item in accordionContent increment :accordionHeight by the item's offsetHeight end -- for increment :accordionHeight by 45 -- to accomodate for py-4 end -- init
on click from me if :accordionOpen is true then trigger closeAccordion on me otherwise trigger openAccordion on me end end -- click
on openAccordion add ${activeStyles} to me add .py-4 .border-t-2 to the #{:accordionBody} if the :accordionHeight is greater than ${maxHeight} transition the #{:accordionBody}'s *height to ${maxHeight} px using 'all ${openDuration} ease-in' add .overflow-y-auto to the #{:accordionBody} otherwise transition the #{:accordionBody} *height to :accordionHeight px using 'all ${openDuration} ease-in' end -- if > maxHeight add .rotate-180 to the #{:twirlIcon} set :accordionOpen to true send closeOpenAccordions to the closest <ul/> end -- openAccordion
on closeAccordion remove ${activeStyles} from me remove .py-4 .border-t-2 from the #{:accordionBody} remove .rotate-180 from the #{:twirlIcon} transition the #{:accordionBody} *height to 0 using 'all ${closeDuration} ease-out' set :accordionOpen to false end -- closeAccordion`
/* Transition calls and toggle .rotate-180 gives an error when using an element reference stored in local variableYou must provide an actual selector instead.
If you add padding to the default styles, the accordion accordionBody has a height equal to the paddingeven if you set h-0. So, padding must be added/removed for the accordion accordionBody to close up fully.The recalcHeight of accordion accordionBody must also take in account the vertical padding value*/---
<li class={tw.wrapper}>
<!-- Accordion Header --> <button id={id} class={tw.accordionHeader} script={hs}> <span>{prefix}{caption}</span> <!-- TwirlIcon --> <svg id={`${id}-twirl-icon`} class={tw.icon} fill='currentColor' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'> <path fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' clip-rule='evenodd'></path> </svg> </button>
<!-- Accordion Body --> <div id={`${id}-body`} class={tw.accordionBody}> <slot /> </div>
</li>Usage
Accordion controls are often used in FAQ or Info sections of a web site. The advantages of using an Accordion is to hide the majority of the content until the user needs to see it.
By presenting just the title in the header the user is prompted to explore the hidden content.