№ 001

Pearl Nav

A pearl bead that rides along the bar on a spring, lifting the surface beneath it. Drag it.

Version
1.0.0
Download
7 KB · 3 files
Added
Oct 26, 2026
Built with
HTML, CSS, JS
Needs
Google Fonts
Tags
tab bar, spring, drag

Usage

Copy pearl-nav.css and pearl-nav.js (and assets/, if there is one) into your project, then:

  new PearlNav(element, { tabs: [{ label, icon, color }], active: 0, onChange(tab, index) {} })
`icon` is a key of PearlNav.icons or a raw SVG string.

Source

/* Pearl nav — a tab bar whose active tab rises out of the surface as a "bead". */

.pn {
  --accent: #d7f23a;
  position: relative;
  width: 100%;
  max-width: 360px;
  height: 64px;
  margin-top: 34px; /* room for the bead + hill above the bar */
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.pn-shape {
  position: absolute;
  left: 0;
  overflow: visible;
  pointer-events: none;
  filter: drop-shadow(0 16px 26px rgba(30, 22, 48, 0.3));
}

.pn-shape path {
  stroke: rgba(255, 255, 255, 0.09);
  stroke-width: 1;
}

.pn-tabs {
  position: absolute;
  inset: 0;
}

.pn-tab {
  position: absolute;
  top: 0;
  height: 100%;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 14px;
  background: none;
  color: #8f889e;
  font: inherit;
  cursor: pointer;
  transition: color 0.2s;
}

.pn-tab:hover { color: #ebe7f2; }

.pn-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -8px;
}

.pn-icon {
  display: grid;
  transition: opacity 0.2s, transform 0.35s cubic-bezier(0.3, 1.5, 0.5, 1);
}

.pn-icon svg { width: 20px; height: 20px; }

.pn-tab[aria-selected="true"] .pn-icon {
  opacity: 0;
  transform: translateY(-12px) scale(0.6);
}

.pn-label {
  position: absolute;
  bottom: 9px;
  left: 50%;
  transform: translate(-50%, 4px);
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.25s, transform 0.3s, color 0.5s;
}

.pn-tab[aria-selected="true"] .pn-label {
  opacity: 1;
  transform: translate(-50%, 0);
}

.pn-bead {
  position: absolute;
  top: 0;
  left: 0;
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: var(--accent);
  background-color: currentColor;
  background-image:
    radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0) 58%),
    radial-gradient(circle at 60% 88%, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0) 60%);
  box-shadow:
    0 12px 30px -6px currentColor,
    inset 0 -2px 4px rgba(0, 0, 0, 0.15);
  cursor: grab;
  touch-action: none;
  will-change: transform;
  transition: color 0.5s;
}

.pn-bead.is-dragging { cursor: grabbing; }

.pn-bead-icon {
  display: grid;
  color: #17140d;
}

.pn-bead-icon svg { width: 22px; height: 22px; }

.pn-bead-icon.pop { animation: pn-pop 0.4s cubic-bezier(0.3, 1.6, 0.5, 1); }

@keyframes pn-pop {
  from { opacity: 0; transform: scale(0.4) rotate(-20deg); }
}

@media (prefers-reduced-motion: reduce) {
  .pn-icon, .pn-label { transition: opacity 0.2s, color 0.5s; }
  .pn-bead-icon.pop { animation: none; }
}