How to Implement a Collapsible Menu Animation with CSS Transitions in Astro-Big-Doc
Astro-Big-Doc achieves smooth collapsible menu animations by transitioning the max-height property from 0 to 200vh on nested lists while rotating arrow icons via CSS transforms, all controlled by a lightweight JavaScript class toggle.
The astro-big-doc template provides a dependency-free solution for animated navigation menus in Astro-based documentation sites. By combining CSS transitions on max-height with minimal JavaScript class manipulation, it creates fluid expand-and-collapse effects without layout thrashing or external animation libraries.
CSS Foundation: The Max-Height Transition Pattern
The animation engine resides in src/layout/SubMenu.astro, where nested unordered lists use a max-height transition rather than animating height directly. This approach avoids expensive layout calculations while guaranteeing smooth 60fps animations.
The relevant CSS rules define two states for the ul.nested class:
ul.nested {
overflow: hidden;
max-height: 200vh; /* large enough when expanded */
transition-property: max-height;
transition-duration: 0.4s;
}
ul.nested.hidden {
max-height: 0px; /* collapsed state */
transition-property: max-height;
transition-duration: 0.4s;
}
When the hidden class is removed, the browser animates max-height from 0 to 200vh over 0.4 seconds, creating a slide-down effect. Adding the class reverses the animation for a slide-up collapse. The 200vh value ensures the menu can accommodate tall content without clipping, while overflow: hidden masks the content during the transition.
JavaScript Toggle Logic
The interactive behavior is handled by src/layout/toc_menu_activation.js. This script registers click listeners on elements with the expand class, then toggles CSS classes to trigger the transitions:
let toggler = document.getElementsByClassName("expand");
for (let i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function (e) {
this.parentElement.parentElement
.querySelector("ul")?.classList.toggle("hidden");
this.parentElement.classList.toggle("expanded");
e.preventDefault();
});
}
The script performs two critical operations on each click:
- Toggles
hiddenon the sibling<ul>– This triggers the CSSmax-heighttransition, expanding or collapsing the submenu. - Toggles
expandedon the container – This activates the arrow rotation animation via CSS, providing visual feedback that the section is open.
Arrow Rotation Animation
Visual feedback for expandable parent items is implemented through CSS transforms on SVG icons within the span.icon elements. The rotation synchronizes with the height transition using identical timing functions:
span.icon > svg {
rotate: 0deg;
transition: rotate .4s ease-in-out;
}
.entry_container.parent.expanded > span > svg {
rotate: 90deg;
}
When the expanded class is added to the parent container, the arrow rotates 90 degrees clockwise over 0.4 seconds with an ease-in-out curve. This creates a cohesive user experience where the directional indicator animates in lockstep with the submenu expansion.
Component Structure and Integration
The SubMenu.astro component renders hierarchical navigation data and applies the necessary classes for the animation system. Each parent entry receives the expand class on its icon container, while nested lists initialize with the hidden class to start in a collapsed state:
---
// src/layout/SubMenu.astro
export interface Props {
items: Array<Object>;
root: boolean;
}
const { items, root = true } = Astro.props;
---
{items && (
<ul class={root ? "root" : "nested hidden"}>
{items.map(item => (
<li>
<div class:list={[
{ entry_container: true, active: item.active, parent: item.parent }
]}>
{item.parent && (
<span class="icon expand">
<!-- Arrow SVG -->
</span>
)}
<span class="text">{item.label}</span>
</div>
<Astro.self items={item.items} root={false} />
</li>
))}
</ul>
)}
<style>
ul.nested {
overflow: hidden;
max-height: 200vh;
transition: max-height 0.4s;
}
ul.nested.hidden {
max-height: 0;
}
</style>
<script src="./toc_menu_activation.js" />
To implement this in your own Astro project, import the component and pass a nested array of menu items:
---
import SubMenu from '../layout/SubMenu.astro';
import menuData from '../data/menu.json';
---
<SubMenu items={menuData} root={true} />
The menu data structure supports recursive nesting through the items property, where each parent node can contain child entries that inherit the same collapsible behavior.
Performance Considerations
Animating max-height rather than height or transform provides specific advantages for documentation navigation:
- No layout thrashing – The browser calculates the layout once at the start and end of the transition, not during every frame.
- GPU acceleration – Modern browsers promote elements with
transformproperties (like the arrow rotation) to compositor layers, ensuring smooth animations even during page scroll. - Content-agnostic – The
200vhmaximum accommodates dynamic content heights without requiring JavaScript height calculations or ResizeObserver APIs.
Summary
src/layout/SubMenu.astrocontains the CSS transition rules usingmax-height: 0tomax-height: 200vhfor smooth slide animations.src/layout/toc_menu_activation.jshandles click events by toggling thehiddenandexpandedclasses, triggering both the height transition and arrow rotation.- The max-height technique avoids layout re-flows and works with dynamic content without JavaScript height calculations.
- Arrow rotation uses CSS
rotatetransforms with identical duration (0.4s) to synchronize visual feedback with menu expansion. - The implementation requires no external dependencies, making it suitable for lightweight documentation sites built with Astro.
Frequently Asked Questions
Why does Astro-Big-Doc use max-height instead of height for the animation?
Using max-height allows the menu to animate smoothly without knowing the exact pixel height of the content beforehand. According to the source code in SubMenu.astro, setting max-height: 200vh accommodates arbitrarily tall submenus while the transition from 0 creates the collapse effect, whereas animating height directly would require expensive JavaScript calculations or fixed dimensions.
How does the arrow rotation stay synchronized with the menu expansion?
The JavaScript in toc_menu_activation.js toggles both the hidden class on the <ul> and the expanded class on the parent container simultaneously. The CSS in SubMenu.astro applies transition: rotate .4s ease-in-out to the arrow SVG and transition: max-height 0.4s to the list, ensuring both properties animate over the identical 0.4-second duration.
Can I customize the animation speed or easing function?
Yes. In SubMenu.astro, modify the transition-duration values in the <style> block (currently set to 0.4s) and adjust the transition-timing-function (currently ease-in-out for arrows). The max-height value of 200vh can also be increased if your menus contain exceptionally large nested structures.
Is this collapsible menu approach accessible for keyboard navigation?
The current implementation in astro-big-doc handles click events via the expand class listeners. For production use, you should enhance toc_menu_activation.js to support keyboard events (Enter and Space keys) on the toggle buttons and add appropriate aria-expanded attributes that update when the hidden class toggles, ensuring screen readers announce state changes correctly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →