gsapdynamicmotion

Dynamic GSAP Dock Navigation

Building a Dynamic Dock Navigation with GSAP and Stateful JavaScript

Playground

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:

  1. Panel stack – hidden content sections
  2. Divider – visual separator
  3. 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.