Velocity.js Elastic Menu — Spring Dropdown Snippet

Velocity.js Elastic Menu · Navigation · Plain HTML, CSS & JS · Live preview

What's included

Features

Physics-based spring easing
easing: 'spring' simulates tension and friction instead of sampling a fixed Bézier curve.
Custom spring tuning
A [tension, friction] array lets you dial exact bounciness per element.
Built-in stagger option
Velocity staggers a NodeList directly — no manual forEach delay loop.
Asymmetric open/close
Opening springs and overshoots; closing eases out fast with no wobble.
Shorthand [end, start] values
translateY and scaleY declare both endpoints in one array per property.
Visibility-aware close
A complete callback hides the panel from the tab order only after the fade finishes.
Click-outside dismissal
A document-level listener closes the menu when focus leaves the nav.
aria-expanded state
The trigger toggles aria-expanded so assistive tech tracks the open state.

About this UI Snippet

Velocity.js Elastic Menu — Spring Easing and Per-Item Stagger Explained

Screenshot of the Velocity.js Elastic Menu snippet rendered live

Most dropdown menus ease open with a plain ease-out curve — quick then slow, never past the target. That reads as "software animating a box." A menu that feels physical needs to overshoot slightly and settle, the way a real hinge or spring would. Velocity.js ships a physics-based spring easing curve out of the box, and this snippet uses it to make a plain dropdown feel tactile without pulling in a full physics library.

Why Velocity instead of CSS transitions

Velocity.js's core API, Velocity(element, properties, options), is a drop-in replacement for jQuery's .animate() that runs on the native DOM and batches style writes so animating dozens of elements doesn't thrash layout. It also exposes easing curves CSS transition-timing-function can't express cleanly — most importantly easing: 'spring', which is not a Bézier curve at all but a physics simulation (tension/friction) that Velocity samples every frame.

The spring easing on the panel

js Velocity(panel, { opacity: 1, translateY: [0, -10], scaleY: [1, 0.85] }, { duration: 600, easing: 'spring' })

Each property array is [endValue, startValue] in Velocity's shorthand — so translateY: [0, -10] explicitly starts the panel 10px above its resting position and animates to 0, while scaleY: [1, 0.85] starts it compressed to 85% height and grows to full scale. With easing: 'spring', that growth doesn't stop cleanly at 1 — it overshoots past 1 by a small margin and oscillates back, which is what makes the panel feel like it's unfolding under tension rather than just fading up.

Per-item stagger without a loop

js Velocity(items, { opacity: 1, translateY: [0, -8] }, { stagger: 60, duration: 500, easing: [220, 16] })

Velocity accepts a NodeList directly as the target and a stagger option in the options object — no manual forEach loop needed. Passing 60 means each subsequent item's animation start is delayed 60ms after the previous one, so the four menu items cascade in visibly rather than popping together. The easing here, [220, 16], is Velocity's shorthand for a custom spring — the first number is tension, the second is friction. Higher tension means a snappier spring; lower friction means more visible overshoot/wobble before it settles. Tuning these two numbers is how you dial the "bounciness" without touching a single keyframe.

Closing is deliberately not the mirror of opening

The close animation uses easing: 'easeOutQuad' and a short 160ms duration for the items, then easeInQuad for the panel with a complete callback that sets visibility: hidden only after the fade finishes. Springs look great appearing but feel sluggish disappearing — nobody wants to wait for an overshoot on the way out — so closing uses fast, no-overshoot easing while opening keeps the spring. The complete callback matters because opacity: 0 alone leaves the panel invisible but still hoverable/tabbable; hiding it after the animation keeps it out of the accessibility tree.

Reusing it

Swap the menu items for a real product catalog or account switcher, or attach the same openMenu/closeMenu pair to a mobile hamburger panel. The spring numbers [220, 16] are worth exposing as a settings control — see how a stiffer [400, 20] feels versus a looser [160, 10].

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet is a good vehicle for understanding easing curves that go beyond CSS. Paste the code into an AI assistant like Claude and ask it to explain the mathematical difference between a spring simulation and a cubic-bezier curve, and why only the former can overshoot past its target value. Then ask what happens if you swap easing: 'spring' for a plain 'ease-out' on the panel — the overshoot disappears and it starts to feel mechanical rather than physical, which is worth seeing side by side. For extension, ask it to add a second custom spring for a "wobble on hover" micro-interaction on each item, make the stagger direction reverse on close (last item closes first), or convert the dropdown into a full mega-menu with multiple columns that each stagger independently.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an "elastic dropdown menu" using Velocity.js (v2, from a CDN, no jQuery) in plain HTML, CSS, and JavaScript.

Requirements:
- A nav bar with a logo and a single trigger button labeled "Products" with a chevron icon that rotates 180deg via CSS transition when open.
- A dropdown panel, absolutely positioned below the trigger, containing 4 menu items each with an icon, a bold title, and a small description line.
- On open: animate the panel with Velocity(panel, { opacity: 1, translateY: [0,-10], scaleY: [1,0.85] }, { duration: 600, easing: 'spring' }) so it uses Velocity's built-in physics-based spring easing (not a Bézier curve) and visibly overshoots slightly before settling.
- Animate the menu items with a single Velocity call targeting the NodeList directly (not a forEach loop) using the built-in stagger option (around 60ms) and a custom spring easing array like [220, 16] (tension, friction) so each item cascades in with its own slight bounce.
- On close: use fast, non-bouncy easing (easeOutQuad/easeInQuad) with a much shorter duration than the open animation — springs should only be used for appearing, not disappearing — and set the panel's visibility to hidden only inside Velocity's complete callback, after the fade finishes, so it leaves the tab order without an abrupt cut.
- Toggle aria-expanded on the trigger and close the menu when a click lands outside the nav element.
- Style it as a dark, premium nav bar with a translucent panel, soft border, and a large soft outer shadow beneath both the bar and the dropdown.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="vem-stage">
  <div class="vem-head">
    <span class="vem-tag">velocity.js · spring easing</span>
    <h2>Elastic Dropdown</h2>
    <p>Open the menu — the panel and every item settle with a springy overshoot, not a linear ease.</p>
  </div>
  <nav class="vem-nav">
    <div class="vem-bar">
      <span class="vem-logo">Northwind</span>
      <button class="vem-trigger" id="vemTrigger" aria-expanded="false">
        <span>Products</span>
        <svg viewBox="0 0 20 20" width="14" height="14"><path d="M5 7l5 5 5-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </button>
    </div>
    <div class="vem-panel" id="vemPanel">
      <a class="vem-item" href="#"><span class="vem-ico">📊</span><div><strong>Analytics</strong><small>Track every metric that matters</small></div></a>
      <a class="vem-item" href="#"><span class="vem-ico">🧩</span><div><strong>Integrations</strong><small>Connect the tools you already use</small></div></a>
      <a class="vem-item" href="#"><span class="vem-ico">🛡️</span><div><strong>Security</strong><small>SOC2-ready from day one</small></div></a>
      <a class="vem-item" href="#"><span class="vem-ico">⚡</span><div><strong>Automations</strong><small>Ship rules without writing code</small></div></a>
    </div>
  </nav>
</div>

Step by step

How to Use

  1. 1
    Add the Velocity.js CDNInclude velocity-animate from the CDN panel — one script tag, no jQuery required.
  2. 2
    Paste HTML, CSS, and JSA nav bar with a Products trigger and a hidden dropdown panel renders closed.
  3. 3
    Click the triggerThe panel scales and translates in with spring easing while items stagger in below it.
  4. 4
    Click outside or the trigger againThe menu closes fast with no overshoot, then hides visibility so it leaves the tab order.
  5. 5
    Tune the springAdjust the [tension, friction] pair in the items easing array to change how bouncy the settle feels.
  6. 6
    Wire real linksReplace the four vem-item anchors with your actual navigation destinations.

Real-world uses

Common Use Cases

Product mega-menus
A springy reveal draws attention to a category dropdown on a marketing site.
Account switchers
Elastic settle gives a workspace picker a premium, tactile feel.
Mobile nav panels
Attach the same open/close pair to a hamburger-triggered side panel.
Filter dropdowns
Any select-like panel benefits from a settle animation instead of a flat show/hide.
Learning Velocity easing
A live reference for tuning spring tension/friction pairs.
Legacy jQuery migrations
Velocity.js is a common drop-in when moving off jQuery .animate() without a rewrite.

Got questions?

Frequently Asked Questions

It is not a Bézier curve — Velocity runs a damped harmonic oscillator simulation (tension and friction) and samples its position every frame. That's why the panel visibly overshoots past its target scale before settling, something a CSS cubic-bezier can't express because those curves are monotonic between 0 and 1 by convention.

They are a custom spring definition: the first number is tension (higher = snappier, faster settle) and the second is friction (lower = more visible bounce/oscillation before settling). This is Velocity-specific shorthand, separate from the named "spring" preset used on the panel.

That's Velocity's shorthand for explicitly setting the start value: the first item in the array is the value to animate TO, the second is the value to animate FROM. It lets you force a starting position (like -10px above rest) without a separate CSS class or a Velocity('scroll') style setup call.

A spring overshoot looks delightful when something appears but feels laggy when dismissing it — you're waiting for a wobble on your way out. Closing uses easeOutQuad/easeInQuad with a short duration so the menu disappears immediately and predictably, while opening keeps the spring for personality.

opacity: 0 makes an element invisible but it is still in the layout and still focusable/tabbable, so keyboard users could tab into a menu they can't see. Setting visibility: hidden after the fade completes removes it from the accessibility tree and tab order without an abrupt visual cut.

Keep a ref to the panel and item elements, call Velocity() imperatively inside a click handler exactly as shown — Velocity works directly on real DOM nodes, so it does not need special framework bindings. Toggle a boolean in state for aria-expanded and initial render visibility, and run the closing Velocity call in a cleanup function if the component can unmount while open.