About this snippet

Build a mesmerizing liquid tab menu. We use SVG filters to create a gooey merge effect when the active pill slides, which adds a highly premium micro-interaction to your UI.

Size
3.1 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
284
<div class="gooey-nav-wrapper">
  
  <!-- Hidden SVG Filter for the Gooey Effect -->
  <svg style="display: none;">
    <defs>
      <filter id="goo-filter">
        <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur"></feGaussianBlur>
        <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -10" result="goo"></feColorMatrix>
        <feBlend in="SourceGraphic" in2="goo"></feBlend>
      </filter>
    </defs>
  </svg>
  
  <div class="nav-container">
    <div class="active-pill" id="active-pill"></div>
    
    <div class="nav-item active-tab" id="tab-1">Dashboard</div>
    <div class="nav-item" id="tab-2">Projects</div>
    <div class="nav-item" id="tab-3">Analytics</div>
    <div class="nav-item" id="tab-4">Settings</div>
  </div>
  
</div>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  margin: 0;
  background: #f1f5f9;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

.gooey-nav-wrapper {
  background: #ffffff;
  padding: 12px;
  border-radius: 40px;
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
  /* Apply the SVG Gooey Filter to the container */
  filter: url("#goo-filter");
}

.nav-container {
  position: relative;
  display: flex;
  gap: 10px;
}

.nav-item {
  position: relative;
  z-index: 2;
  padding: 12px 24px;
  font-weight: 600;
  font-size: 1rem;
  color: #64748b;
  cursor: pointer;
  border-radius: 30px;
  transition: color 0.3s ease;
  user-select: none;
}

.nav-item.active-tab {
  color: #ffffff;
}

.active-pill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 110px; /* Default width, dynamically updated by JS */
  background: #3b82f6;
  border-radius: 30px;
  z-index: 1;
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), width 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  will-change: transform, width;
}
const tabs = document.querySelectorAll(".nav-item");
const pill = document.getElementById("active-pill");
const container = document.querySelector(".nav-container");

function updatePill(targetTab) {
  /* Remove active class from all */
  tabs.forEach(function(t) {
    t.classList.remove("active-tab");
  });
  
  /* Add active class to clicked */
  targetTab.classList.add("active-tab");
  
  /* Calculate physical bounds for perfect pill alignment */
  const tabRect = targetTab.getBoundingClientRect();
  const containerRect = container.getBoundingClientRect();
  
  const offsetLeft = tabRect.left - containerRect.left;
  const newWidth = tabRect.width;
  
  /* Slide and stretch the background pill */
  pill.style.width = newWidth + "px";
  pill.style.transform = "translateX(" + offsetLeft + "px)";
}

/* Initialize first tab */
updatePill(document.getElementById("tab-1"));

tabs.forEach(function(tab) {
  tab.addEventListener("click", function() {
    updatePill(tab);
  });
});

window.addEventListener("resize", function() {
  const active = document.querySelector(".active-tab");
  if (active) updatePill(active);
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save