№ 016

Dimple Nav

A soft ivory clay tab bar: press a tab and the surface sinks into a crater around a lit graphite switch, while the old one fills back in.

Version
1.0.0
Download
13 KB · 3 files
Added
Oct 26, 2026
Built with
HTML, CSS, JS
Needs
Google Fonts
Tags
tab bar, tactile, webgl

Usage

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

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

A tactile tab bar made of soft ivory clay. Pressing a tab punches a crater into the surface where
it is — a circular dent spreads out, sinks into a bowl and a graphite switch with a lime light
settles inside — while the old crater fills itself back in. Nothing travels: the surface morphs.

The clay is a height field lit in a WebGL fragment shader (real normals, a cast shadow along the
light, occlusion in the bowl and a chamfered slab with a visible front wall), so the shading stays right at every moment of the deformation.
Icons and switches are plain DOM on top. Without WebGL the bar falls back to a flat CSS pill.

Source

/* Dimple nav — ivory clay tab bar; pressing a tab punches a crater with a lit graphite switch in it. */

.dn {
  position: relative;
  width: 100%;
  max-width: 640px;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.dn-bar {
  position: relative;
  width: 100%;
  aspect-ratio: 3.6;
}

/* the slab's footprint: carries the ambient shadow, and is the whole bar when WebGL is missing */
.dn-shadow {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: #e6dfd1;
  box-shadow:
    0 1px 1px rgba(70, 52, 30, 0.35),
    0 calc(var(--dn-h) * 0.03) calc(var(--dn-h) * 0.04) rgba(70, 52, 30, 0.3),
    0 calc(var(--dn-h) * 0.1) calc(var(--dn-h) * 0.14) calc(var(--dn-h) * -0.02) rgba(70, 52, 30, 0.38),
    0 calc(var(--dn-h) * 0.24) calc(var(--dn-h) * 0.32) calc(var(--dn-h) * -0.06) rgba(70, 52, 30, 0.3),
    0 calc(var(--dn-h) * 0.45) calc(var(--dn-h) * 0.6) calc(var(--dn-h) * -0.14) rgba(70, 52, 30, 0.18);
}

.dn-surface {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.dn-flat .dn-surface { display: none; }

.dn-flat .dn-shadow {
  background: linear-gradient(180deg, #f6f1e7, #e9e1d2 70%, #d8cdb9);
}

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

.dn-tab {
  position: absolute;
  top: var(--dn-face, 46%);
  width: calc(var(--dn-h) * 0.62);
  height: calc(var(--dn-h) * 0.62);
  transform: translate(-50%, -50%);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: #1c1a17;
  cursor: pointer;
  touch-action: manipulation;
}

.dn-tab.is-active { cursor: default; }

.dn-tab:focus-visible { outline: none; }

.dn-tab:focus-visible::after {
  content: "";
  position: absolute;
  inset: calc(var(--dn-h) * -0.02);
  border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(38, 35, 32, 0.55);
}

.dn-icon {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  will-change: transform, opacity;
}

.dn-icon svg {
  width: calc(var(--dn-h) * 0.245);
  height: calc(var(--dn-h) * 0.245);
  overflow: visible;
  /* printed into the clay: a hairline of light catches the lower edge of each stroke */
  filter: drop-shadow(0 1px 0 rgba(255, 252, 244, 0.7)) drop-shadow(0 -0.5px 0 rgba(90, 70, 40, 0.25));
}

.dn-tab:not(.is-active):hover .dn-icon svg { transform: translateY(-1px); }
.dn-icon svg { transition: transform 0.2s; }

/* the graphite switch: a matte puck with a bevelled outer ring, and a neon lens in its face */
.dn-switch {
  --dn-lit: 0;
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--dn-h) * 0.5);
  height: calc(var(--dn-h) * 0.5);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  background:
    /* lime light spilling over the face around the lens */
    radial-gradient(circle, rgba(196, 245, 52, calc(0.1 * var(--dn-lit))) 0 22%, rgba(196, 245, 52, calc(0.03 * var(--dn-lit))) 32%, transparent 44%),
    /* soft top-light falloff */
    linear-gradient(180deg, rgba(255, 255, 255, 0.07), transparent 45%, rgba(0, 0, 0, 0.18)),
    /* face, the groove, the bevelled ring and its outer edge */
    radial-gradient(circle, #333331 0 63%, #1b1b1a 67%, #2a2a28 70%, #575753 76%, #3a3a36 84%, #232321 95%, #1a1a19 100%);
  box-shadow:
    inset 0 calc(var(--dn-h) * 0.006) 0 rgba(255, 255, 255, 0.14),
    inset 0 calc(var(--dn-h) * -0.01) calc(var(--dn-h) * 0.014) rgba(0, 0, 0, 0.5),
    /* the crevice where it sits in the clay */
    0 0 0 1px rgba(40, 30, 18, 0.35),
    0 calc(var(--dn-h) * 0.008) calc(var(--dn-h) * 0.012) rgba(26, 18, 8, 0.7),
    0 calc(var(--dn-h) * 0.03) calc(var(--dn-h) * 0.05) rgba(50, 36, 18, 0.45);
}

/* matte grain on the puck */
.dn-switch::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0.3;
  mix-blend-mode: overlay;
  -webkit-mask: radial-gradient(circle, #000 0 64%, transparent 66%);
  mask: radial-gradient(circle, #000 0 64%, transparent 66%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

.dn-led {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--dn-h) * 0.19);
  height: calc(var(--dn-h) * 0.19);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 50% 46%, #e2ff74 0%, #cdf93a 40%, #c3f22e 80%, #b2e223 100%);
  filter: brightness(calc(0.42 + var(--dn-lit) * 0.58)) saturate(calc(0.35 + var(--dn-lit) * 0.65));
  box-shadow:
    inset 0 0 0 1px rgba(150, 190, 20, 0.5),
    inset 0 -1px 2px rgba(80, 110, 0, 0.35),
    0 0 calc(var(--dn-h) * 0.012 * var(--dn-lit)) rgba(214, 252, 70, calc(0.55 * var(--dn-lit))),
    0 0 calc(var(--dn-h) * 0.06 * var(--dn-lit)) rgba(196, 245, 50, calc(0.18 * var(--dn-lit)));
  animation: dn-breathe 3.2s ease-in-out infinite;
}

@keyframes dn-breathe {
  50% { scale: 0.97; }
}

/* lime light spilling onto the surface below the bar */
.dn-glows {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.dn-glow {
  position: absolute;
  top: calc(var(--dn-h) * 1.24);
  width: calc(var(--dn-h) * 0.85);
  height: calc(var(--dn-h) * 0.6);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  opacity: 0;
  background: radial-gradient(closest-side, rgba(220, 255, 80, 0.85), rgba(196, 240, 50, 0.42) 40%, rgba(180, 225, 40, 0.12) 70%, transparent);
  filter: blur(calc(var(--dn-h) * 0.04));
}

@media (prefers-reduced-motion: reduce) {
  .dn-led { animation: none; }
}