Breakdown
Modern interfaces often rely on compact navigation systems that reveal contextual content without redirecting users to a new page. One such pattern is the dock navigation, where tabs trigger expandable panels while maintaining spatial continuity.
In this tutorial, we’ll build a dynamic dock menu that:
• expands panels based on selected tabs
• animates tab highlighting with GSAP
• shows contextual hover indicators for items
• gracefully handles keyboard and pointer interactions
The result is a compact, state-driven navigation component that feels smooth and responsive.
UI Structure Overview
The dock layout contains three main sections:
- Panel stack – hidden content sections
- Divider – visual separator
- Tab navigation – interactive controls
1<aside class="dock" id="dock">
2
3 <div class="panel-stack">
4 <section class="panel" data-panel="apps"></section>
5 <section class="panel" data-panel="components"></section>
6 <section class="panel" data-panel="notes"></section>
7 </div>
8
9 <div class="divider"></div>
10
11 <nav class="nav">
12 <span class="pill"></span>
13
14 <button class="tab" data-panel="apps">Apps</button>
15 <button class="tab" data-panel="components">Components</button>
16 <button class="tab" data-panel="notes">Notes</button>
17 </nav>
18
19</aside>Each tab corresponds to a panel through the data-panel attribute.
Panel System
Each panel contains a list of interactive items.
HTML
1<section class="panel" data-panel="apps">
2 <article class="item">
3 <div class="icon">K</div>
4 <div class="title">Klack</div>
5 <div class="subtitle">Satisfying key sounds</div>
6 </article>
7</section>Panels remain hidden by default and become visible when activated.
CSS
1.panel {
2 block-size: 0;
3 opacity: 0;
4 transform: translateY(0.5rem);
5}
6
7.panel.is-active {
8 block-size: auto;
9 opacity: 1;
10 transform: translateY(0);
11}This creates a smooth expanding panel animation.
Dock Expansion
When a tab is selected, the dock expands and reveals the panel stack.
CSS
1.dock.is-open {
2 row-gap: 1rem;
3}A pseudo-element expands the background container.
CSS
1.dock::before {
2 content: "";
3 position: absolute;
4 inset: 0;
5 transition: inset 350ms cubic-bezier(0.65,0,0.35,1);
6}This makes the dock feel like it physically grows when opened.
Animated Tab Highlight (Pill)
The active tab is highlighted using a moving pill background.
HTML
1<span class="pill"></span>Instead of instantly jumping between tabs, GSAP animates the pill.
JavaScript
1gsap.to(pill, {
2 "--pill-x": `${tab.offsetLeft}px`,
3 "--pill-width": `${tab.offsetWidth}px`,
4 duration: 0.25,
5 ease: "power3.inOut"
6});This creates a smooth sliding highlight effect.
Item Hover Indicator
Each panel contains a hover indicator that moves beneath hovered items.
The indicator is dynamically injected into each panel:
JavaScript
1const indicator = document.createElement("span");
2indicator.className = "item-hover-indicator";
3panel.prepend(indicator);When hovering an item, the indicator moves to match the item’s geometry.
JavaScript
1gsap.to(indicator, {
2 "--hover-x": `${item.offsetLeft}px`,
3 "--hover-y": `${item.offsetTop}px`,
4 "--hover-width": `${item.offsetWidth}px`,
5 "--hover-height": `${item.offsetHeight}px`
6});This gives the UI a magnetic hover effect.
Component Architecture
The entire interaction system is encapsulated in a class.
JavaScript
1class DockNavigation {
2 constructor() {
3 this.#init();
4 }
5}This keeps logic modular and easier to maintain.
The component manages three key pieces of state:
JavaScript
1#state = {
2 activePanelId: null,
3 isOpen: false
4};This state determines:
• which panel is visible
• whether the dock is open
Tab Interaction Logic
When a tab is clicked, the dock updates its state.
JavaScript
1#handleTabSelect(panelId) {
2 if (this.#state.isOpen && panelId === this.#state.activePanelId) {
3 this.#closeMenu();
4 return;
5 }
6
7 this.#setMenuState(true, panelId);
8}Clicking the same tab twice closes the dock, which mirrors behavior seen in many modern UI systems.
Outside Click Handling
To improve usability, clicking outside the dock automatically closes it.
JavaScript
1document.addEventListener("pointerdown", (event) => {
2 if (!dock.contains(event.target)) {
3 closeMenu();
4 }
5});This prevents the interface from remaining open unintentionally.
Keyboard Accessibility
The dock also supports keyboard navigation.
Pressing Escape closes the menu.
JavaScript
1document.addEventListener("keydown", (event) => {
2 if (event.key === "Escape") {
3 closeMenu();
4 }
5});Accessibility support like this ensures the component works beyond pointer devices.
Motion Optimization
The component respects reduced motion preferences.
JavaScript
1prefersReducedMotion() {
2 return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3}Animations automatically disable when necessary, improving accessibility.
Why This Pattern Works
This dock navigation works well because it:
• keeps navigation compact
• provides contextual content without page reloads
• uses motion to reinforce interaction
• separates structure, styling, and state logic
The result feels closer to a desktop interface than a traditional website menu.
Final Thoughts
Complex UI interactions don’t need complex markup. By combining stateful JavaScript architecture, GSAP animations, and thoughtful CSS layout, you can build navigation systems that feel fluid and intuitive.
Interfaces like this bridge the gap between web navigation and application UI, creating a richer user experience.