How do you build animated menus that stay accessible?

Navigation effects make menus, tabs and tooltips easier to follow: underlines that slide to the active link, hamburgers that morph into a close icon, dropdowns and accordions that expand smoothly. They use CSS transitions, the details element or small scripts, and CSS anchor positioning, where supported, to attach tooltips to their trigger.

How to choose, performance and accessibility
  • Underline Expand and Pill Indicator suit a site header, Morphing Hamburger a mobile menu, Tab Slider a pricing or feature switcher, and Sticky Header Detection a long page.
  • Every menu must open, close with Escape and move focus correctly from the keyboard, expose its state with aria-expanded, and remain usable without animation.
  • Underline Expand, Accordion Basic, Smart Tooltip System and Connected Nodes are free with their full code; the other navigation components come with Premium.

Underline Expand

Line that extends from the center.

CSS Center
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="nav-demo">
    <span class="nav-link-demo link-underline-expand">Home</span>
    <span class="nav-link-demo link-underline-expand">Services</span>
    <span class="nav-link-demo link-underline-expand">Contact</span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%);
  --space-2: 0.5rem;
  --space-8: 2rem;
  --text-primary: #ffffff;
  --text-secondary: #a1a1aa;
}

.nav-demo {
  display: flex;
  gap: var(--space-8);
  align-items: center;
}

.nav-link-demo {
  color: var(--text-secondary);
  font-weight: 500;
  padding: var(--space-2) 0;
  position: relative;
  cursor: pointer;
  transition: color 0.3s;
}

.nav-link-demo:hover {
  color: var(--text-primary);
}

.link-underline-expand::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 0;
  height: 2px;
  background: var(--gradient-primary);
  transition: all 0.3s ease;
  transform: translateX(-50%);
}

.link-underline-expand:hover::after {
  width: 100%;
}
JavaScript
/* Pure CSS — no JavaScript required */

Underline Slide

Line that slides from right to left.

CSS Slide

Background Fill

Background that fills on hover.

CSS Fill

Bracket Hover

Brackets that appear on hover.

CSS Text

Hamburger Menu

Hamburger to cross animation.

CSS Toggle

Pill Indicator

Animated indicator that follows the selection.

JS Tab

Dropdown Reveal

Dropdown menu with fade/slide animation.

CSS Dropdown

Morphing Hamburger

Hamburger icon that morphs into an X with smooth animation.

CSS Morph

Tab Slider

Tabs with sliding indicator that follows the active tab.

JS Slider

Breadcrumb Animated

Animated breadcrumb with interactive chevrons.

CSS Breadcrumb

Sticky Header Detection

Sticky header with Intersection Observer that detects scroll and changes style.

JS Sticky Observer

Custom Bezier Transition

Color transition with custom cubic-bezier curve for smooth movement.

CSS Bezier Smooth

Floating Indicator

Floating indicator with smooth cubic-bezier transition. Inspired by CTA Gallery.

JS Nav Smooth

Stagger Menu Reveal

Menu with staggered animation - each item appears with a progressive delay.

CSS Stagger Reveal
Content of the first section with smooth height animation.
Second section with different content.
Third and final section.

Accordion Basic

Expandable sections with smooth height animation. Multiple sections can be open.

CSS JS Interactive
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="component-demo">
    <div class="accordion" id="accordionBasic">
      <div class="accordion-item active">
        <button class="accordion-header" onclick="toggleAccordion(this, 'basic')">
          Section 1
          <svg class="accordion-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M6 9l6 6 6-6"/>
          </svg>
        </button>
        <div class="accordion-content">
          <div class="accordion-body">
            Content of the first section with smooth height animation.
          </div>
        </div>
      </div>
      <div class="accordion-item">
        <button class="accordion-header" onclick="toggleAccordion(this, 'basic')">
          Section 2
          <svg class="accordion-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M6 9l6 6 6-6"/>
          </svg>
        </button>
        <div class="accordion-content">
          <div class="accordion-body">
            Second section with different content.
          </div>
        </div>
      </div>
      <div class="accordion-item">
        <button class="accordion-header" onclick="toggleAccordion(this, 'basic')">
          Section 3
          <svg class="accordion-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M6 9l6 6 6-6"/>
          </svg>
        </button>
        <div class="accordion-content">
          <div class="accordion-body">
            Third and final section.
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.component-demo {
  width: 100%;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.accordion {
  width: 100%;
  max-width: 350px;
}

.accordion-item {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  margin-bottom: 8px;
  overflow: hidden;
}

.accordion-header {
  width: 100%;
  padding: 16px 20px;
  background: transparent;
  border: none;
  color: white;
  font-size: 0.95rem;
  font-weight: 500;
  font-family: inherit;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  transition: background 0.3s ease;
}

.accordion-header:hover {
  background: rgba(255, 255, 255, 0.05);
}

.accordion-icon {
  transition: transform 0.3s ease;
}

.accordion-item.active .accordion-icon {
  transform: rotate(180deg);
}

.accordion-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.4s ease, padding 0.3s ease;
}

.accordion-item.active .accordion-content {
  max-height: 200px;
}

.accordion-body {
  padding: 0 20px 16px;
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.9rem;
  line-height: 1.6;
}
JavaScript
(function() {
  function toggleAccordion(header, type) {
    const item = header.parentElement;
    const accordion = item.parentElement;

    if (type === 'single') {
      // Close all other items in single mode
      accordion.querySelectorAll('.accordion-item').forEach(otherItem => {
        if (otherItem !== item) {
          otherItem.classList.remove('active');
        }
      });
    }

    item.classList.toggle('active');
  }
  window.toggleAccordion = toggleAccordion;
})();

Accordion Single

Only one section open at a time. Others close automatically.

CSS JS Exclusive

Tabs Animated

Tabs with indicator that slides between selections.

CSS JS Transition

Toast Success

Green success notification with slide animation. Disappears after 3 seconds.

JS Animation Feedback

Toast Error

Red error notification to alert the user. Auto-dismiss after 3 seconds.

JS Animation Alert

Toast Stack

Multiple toasts stack up with elegant spacing.

JS Stack Queue

Skeleton Card

Card placeholder with shimmer effect during loading.

CSS Loading Shimmer

Skeleton Text

Text placeholder with elegant loading animation.

CSS Loading Placeholder

Tooltip Fade

Tooltip that fades in on hover.

CSS Hover Fade

Tooltip Arrow

Tooltip with arrow pointer and rise animation.

CSS Hover Arrow

Modal Slide

Modal that slides from the bottom of the screen with blur overlay.

CSS JS Overlay

Dropdown Animated

Dropdown menu with cascading reveal animation.

CSS JS Stagger

Smart Tooltip System

Tooltips that automatically flip based on viewport position using position-try-fallbacks.

CSS JS Anchor
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="anchor-demo">
    <div class="tooltip-scene" id="tooltipScene">
      <button class="anchor-tooltip-trigger" data-tooltip-pos="top">
        Top
        <span class="smart-tooltip">Info above</span>
      </button>
      <button class="anchor-tooltip-trigger" data-tooltip-pos="top">
        Edge
        <span class="smart-tooltip">Auto-flip if out of viewport</span>
      </button>
      <button class="anchor-tooltip-trigger" data-tooltip-pos="top">
        Smart
        <span class="smart-tooltip">Adaptive position</span>
      </button>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.anchor-demo {
  position: relative;
  width: 100%;
  height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.anchor-demo * {
  box-sizing: border-box;
}

.tooltip-scene {
  display: flex;
  gap: 32px;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  padding: 20px;
}

.anchor-tooltip-trigger {
  position: relative;
  padding: 10px 18px;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
}

.anchor-tooltip-trigger .smart-tooltip {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(100% + 10px);
  padding: 8px 14px;
  background: #1e1b4b;
  color: #e0e7ff;
  font-size: 0.78rem;
  font-weight: 500;
  border-radius: 8px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 100;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

.anchor-tooltip-trigger .smart-tooltip::after {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 100%;
  border: 6px solid transparent;
  border-top-color: #1e1b4b;
}

.anchor-tooltip-trigger:hover .smart-tooltip,
.anchor-tooltip-trigger:focus .smart-tooltip {
  opacity: 1;
}
JavaScript
/* Pure CSS — no JavaScript required */
A
B
C
D

Connected Nodes

Draggable nodes connected by SVG lines that update in real time.

SVG JS Drag
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="anchor-demo">
    <div class="nodes-scene" id="nodesScene">
      <svg class="node-lines">
        <line id="line-ab" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-bc" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-cd" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-da" x1="0" y1="0" x2="0" y2="0"/>
      </svg>
      <div class="node-point node-a" data-node="a">A</div>
      <div class="node-point node-b" data-node="b">B</div>
      <div class="node-point node-c" data-node="c">C</div>
      <div class="node-point node-d" data-node="d">D</div>
    </div>
  </div>
</div>
CSS
.anchor-demo  {
   position: relative;
   width: 100%;
   height: 220px;
   display: flex;
   align-items: center;
   justify-content: center;
   overflow: hidden;
   }

.anchor-demo *  {
   box-sizing: border-box;
   }

.nodes-scene  {
   position: relative;
   width: 100%;
   height: 100%;
   }

.nodes-scene svg.node-lines  {
   position: absolute;
   inset: 0px;
   width: 100%;
   height: 100%;
   pointer-events: none;
   }

.nodes-scene svg.node-lines line  {
   stroke: rgb(129, 140, 248);
   stroke-width: 2;
   stroke-dasharray: 6, 4;
   opacity: 0.6;
   }

.node-point  {
   position: absolute;
   width: 48px;
   height: 48px;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 0.75rem;
   font-weight: 700;
   color: rgb(255, 255, 255);
   cursor: grab;
   user-select: none;
   z-index: 2;
   transition: box-shadow 0.2s;
   }

.node-point:active  {
   cursor: grabbing;
   }

.node-point:hover  {
   box-shadow: rgba(129, 140, 248, 0.6) 0px 0px 20px;
   }

.node-a  {
   background: linear-gradient(135deg, rgb(99, 102, 241), rgb(129, 140, 248));
   top: 20%;
   left: 15%;
   }

.node-b  {
   background: linear-gradient(135deg, rgb(236, 72, 153), rgb(244, 114, 182));
   top: 60%;
   left: 55%;
   }

.node-c  {
   background: linear-gradient(135deg, rgb(16, 185, 129), rgb(52, 211, 153));
   top: 25%;
   left: 70%;
   }

.node-d  {
   background: linear-gradient(135deg, rgb(245, 158, 11), rgb(251, 191, 36));
   top: 70%;
   left: 20%;
   }
JavaScript
(function() {
  // ============================================
  // 2. Connected Nodes - draggable with SVG lines
  // ============================================
  (function(){
    var scene = document.getElementById('nodesScene');
    if(!scene) return;
    var nodes = scene.querySelectorAll('.node-point');
    var lineAb = document.getElementById('line-ab');
    var lineBc = document.getElementById('line-bc');
    var lineCd = document.getElementById('line-cd');
    var lineDa = document.getElementById('line-da');
    if(!lineAb || !lineBc || !lineCd || !lineDa) return;
    var lines = { ab: lineAb, bc: lineBc, cd: lineCd, da: lineDa };
    var nodeMap = {};
    nodes.forEach(function(n){ nodeMap[n.dataset.node] = n; });

    function getCenter(el){
      var r = el.getBoundingClientRect();
      var sr = scene.getBoundingClientRect();
      return { x: r.left - sr.left + r.width/2, y: r.top - sr.top + r.height/2 };
    }
    function updateLines(){
      var a=getCenter(nodeMap.a), b=getCenter(nodeMap.b), c=getCenter(nodeMap.c), d=getCenter(nodeMap.d);
      lines.ab.setAttribute('x1',a.x); lines.ab.setAttribute('y1',a.y);
      lines.ab.setAttribute('x2',b.x); lines.ab.setAttribute('y2',b.y);
      lines.bc.setAttribute('x1',b.x); lines.bc.setAttribute('y1',b.y);
      lines.bc.setAttribute('x2',c.x); lines.bc.setAttribute('y2',c.y);
      lines.cd.setAttribute('x1',c.x); lines.cd.setAttribute('y1',c.y);
      lines.cd.setAttribute('x2',d.x); lines.cd.setAttribute('y2',d.y);
      lines.da.setAttribute('x1',d.x); lines.da.setAttribute('y1',d.y);
      lines.da.setAttribute('x2',a.x); lines.da.setAttribute('y2',a.y);
    }
    updateLines();

    var dragging = null, offsetX = 0, offsetY = 0;
    nodes.forEach(function(n){
      n.addEventListener('mousedown', function(e){
        e.preventDefault();
        dragging = n;
        var r = n.getBoundingClientRect();
        offsetX = e.clientX - r.left;
        offsetY = e.clientY - r.top;
      });
      n.addEventListener('touchstart', function(e){
        e.preventDefault();
        dragging = n;
        var r = n.getBoundingClientRect();
        var t = e.touches[0];
        offsetX = t.clientX - r.left;
        offsetY = t.clientY - r.top;
      }, {passive:false});
    });
    function onMove(cx, cy){
      if(!dragging) return;
      var sr = scene.getBoundingClientRect();
      var x = cx - sr.left - offsetX;
      var y = cy - sr.top - offsetY;
      x = Math.max(0, Math.min(sr.width - 48, x));
      y = Math.max(0, Math.min(sr.height - 48, y));
      dragging.style.left = x + 'px';
      dragging.style.top = y + 'px';
      updateLines();
    }
    document.addEventListener('mousemove', function(e){ onMove(e.clientX, e.clientY); });
    document.addEventListener('touchmove', function(e){ if(dragging){ e.preventDefault(); onMove(e.touches[0].clientX, e.touches[0].clientY); }}, {passive:false});
    document.addEventListener('mouseup', function(){ dragging = null; });
    document.addEventListener('touchend', function(){ dragging = null; });

    // Animate initial line draw
    setTimeout(updateLines, 100);
  })();
})();

Floating Action Labels

Floating labels anchored to action buttons that appear on hover with scroll-aware repositioning.

CSS Hover Position

Popover Menu Chain

Chained popover menus with submenus positioned relative to their parent using the Popover API pattern.

JS Popover Menu

Annotation Layer

Floating annotations anchored to specific content areas with smart stacking.

CSS Hover Annotation

Dynamic Badge Positioning

Notification badges anchored to avatar corners, adapting to resize.

CSS Badge Responsive

Guided Tour Spotlight

Step-by-step onboarding with spotlight anchored to UI elements and arrow indicator.

JS Tour Anchor

Context Menu

Context menu positioned at cursor with edge-of-viewport flip logic.

JS Click Position

Footnote Popover

Academic footnotes appearing as popovers near reference markers.

CSS Hover Popover

Chart Data Labels

Data labels anchored to chart points with automatic overlap avoidance.

CSS Hover DataViz

Command Palette ⌘K

⌘K command palette built on the native popover API: filtering search, keyboard navigation, light-dismiss. The modern SaaS pattern, without the JS overhead.

💡 Perfect for: fast SaaS app navigation, global search, power-user shortcuts, action menu, back-office.

Popover API ⌘K Navigation Premium

Frequently Asked Questions

How to create an animated hamburger menu in CSS? +

Use 3 spans inside a button. On click, apply transforms: the middle span disappears (opacity:0), the first rotates 45deg and the third -45deg. Use transition: all 0.3s for a smooth animation.

How to build a CSS mega-menu? +

Place the mega-menu in absolute position below the parent item. Use an invisible div (pseudo-element ::before) between the link and the menu to prevent the hover from being lost. Animate the appearance with opacity and transform.

What is the best practice for a mobile menu? +

Use a side panel (sidebar) that slides in from the left or right with transform: translateX(). Add a semi-transparent overlay. Close on overlay click, close button, and Escape key. Lock the body scroll.

How to make a keyboard-accessible menu? +

Each item must be navigable with Tab. Submenus open with Enter or Arrow Down. Escape closes the submenu. Add aria-expanded on toggle buttons and role=navigation on the nav element.

How to create animated CSS tabs? +

Use hidden radio buttons (:checked) or JavaScript to manage the active state. Animate an indicator (pseudo-element) under the active tab with transition on left and width. Content changes with a fade or slide.

Similar Categories