← Back to archive
Design Engineering · 2026-09-04

Simulating Real Optics: How We Built the Glass Navbar

Moving beyond basic backdrop-blur: combining dynamic SVG displacement maps, chromatic aberration, and GPU compositing for tactile glass.

Most web interfaces settle for a caricature of glass: slap on backdrop-filter: blur(16px), lower the background alpha to 15%, add a 1px border, and call it glassmorphism.

While that creates a passable frosted plastic look, it completely ignores how actual physical glass behaves. Real glass does not merely blur whatever lies beneath it—it bends light rays along surface curvatures (refraction) and disperses white light into spectral fringes near the edges (chromatic aberration).

When designing the floating pill navbar for this site, we wanted something that felt tactile, optical, and alive. Here is a technical breakdown of how we achieved true optical glass using dynamic SVG displacement maps, multi-pass chromatic separation, and GPU-conscious CSS.


1. The Anatomy of Real Glass

To recreate glass authentically in the browser, we have to simulate three optical phenomena:

  1. Refraction & Edge Curvature: Light passes through the flat center of a glass pane relatively undisturbed. But near curved chamfers and rounded pill edges, light rays bend sharply inward.
  2. Chromatic Aberration (Dispersion): Different wavelengths of light have different refractive indices. Blue light bends at a slightly different angle than red light, creating subtle color fringing at the perimeter of the lens.
  3. Internal Reflections & Specular Highlights: Microscopic bevels along the edge catch light, creating a crisp hairline rim, while ambient drop shadows separate the glass pane from the canvas behind it.

Standard CSS cannot do refraction on its own. However, the SVG filter specification provides <feDisplacementMap>, and modern browsers allow CSS backdrop-filter to target SVG filter definitions:

backdrop-filter: url(#glass-filter) saturate(1);

The challenge is building a displacement map that conforms precisely to dynamic pill dimensions, and then running chromatic dispersion in real time.


2. Generating the Vector Displacement Map on the Fly

A static displacement map image won't work because our navbar has dynamic dimensions (width: fit-content) that adapt to screen sizes, font metrics, and navigation state.

To solve this, we generate an inline SVG vector field dynamically in Svelte via buildMap(width, height):

const radius = 20;
const blur = 7;
const insetRatio = 0.035;

function buildMap(width: number, height: number) {
  const inset = height * insetRatio;
  return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="x" x1="100%" y1="0%" x2="0%" y2="0%">
        <stop offset="0%" stop-color="#0000"/>
        <stop offset="100%" stop-color="red"/>
      </linearGradient>
      <linearGradient id="y" x1="0%" y1="0%" x2="0%" y2="100%">
        <stop offset="0%" stop-color="#0000"/>
        <stop offset="100%" stop-color="blue"/>
      </linearGradient>
    </defs>
    <rect width="${width}" height="${height}" fill="black"/>
    <rect width="${width}" height="${height}" rx="${radius}" fill="url(#x)"/>
    <rect width="${width}" height="${height}" rx="${radius}" fill="url(#y)" style="mix-blend-mode:screen"/>
    <rect x="${inset}" y="${inset}" width="${width - inset * 2}" height="${height - inset * 2}" rx="${radius}" fill="hsl(0 0% 50% / .93)" style="filter:blur(${blur}px)"/>
  </svg>`;
}

How the map works:

  • X and Y coordinate encoding: Red encodes horizontal displacement vectors, while Blue encodes vertical displacement vectors. Blending them with mix-blend-mode: screen creates a composite vector map.
  • The Inset Neutralizer: In an <feDisplacementMap>, a color value of 50% gray (rgb(128, 128, 128)) represents zero displacement. By layering an inset rounded rectangle filled with hsl(0 0% 50% / .93) and blurred by 7px, the center of the navbar remains distortion-free and legible, while the refractive distortion ramps up smoothly toward the outer 20px pill corner radius.

3. Synchronizing Dimensions with ResizeObserver

Because the vector map relies on the exact bounding box of the navbar element, we attach a ResizeObserver on mount. When the navbar is measured or resized (or when web fonts finish loading), the SVG map is serialized into a data URI and injected directly into the filter's <feImage>:

onMount(() => {
  const syncMap = () => {
    const { width, height } = glass.getBoundingClientRect();
    if (!width || !height) return;
    const href = `data:image/svg+xml,${encodeURIComponent(buildMap(width, height))}`;
    mapImage.setAttribute('href', href);
    mapImage.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', href);
  };

  const observer = new ResizeObserver(syncMap);
  observer.observe(glass);
  document.fonts?.ready.then(syncMap);
  syncMap();

  return () => observer.disconnect();
});

4. The Chromatic Dispersion Pipeline

In optical physics, chromatic aberration occurs because glass has different refractive indices for different light frequencies.

We model this by splitting the backdrop's SourceGraphic into three parallel displacement passes, each with a slightly offset displacement scale:

<svg class="glass-svg" aria-hidden="true" focusable="false">
  <defs>
    <filter id="glass-filter" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%">
      <!-- 1. Feed dynamic displacement map -->
      <feImage bind:this={mapImage} x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />

      <!-- 2. Displace and isolate Red channel (scale: -180) -->
      <feDisplacementMap in="SourceGraphic" in2="map" result="dispRed" scale="-180" xChannelSelector="R" yChannelSelector="G" />
      <feColorMatrix in="dispRed" type="matrix" result="red" values="1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0" />

      <!-- 3. Displace and isolate Green channel (scale: -170) -->
      <feDisplacementMap in="SourceGraphic" in2="map" result="dispGreen" scale="-170" xChannelSelector="R" yChannelSelector="G" />
      <feColorMatrix in="dispGreen" type="matrix" result="green" values="0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0" />

      <!-- 4. Displace and isolate Blue channel (scale: -160) -->
      <feDisplacementMap in="SourceGraphic" in2="map" result="dispBlue" scale="-160" xChannelSelector="R" yChannelSelector="G" />
      <feColorMatrix in="dispBlue" type="matrix" result="blue" values="0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0" />

      <!-- 5. Recombine RGB channels via additive screen blend -->
      <feBlend in="red" in2="green" mode="screen" result="rg" />
      <feBlend in="rg" in2="blue" mode="screen" result="output" />

      <!-- 6. Micro-soften anti-aliasing artifacts -->
      <feGaussianBlur in="output" stdDeviation="0.5" />
    </filter>
  </defs>
</svg>

By displacing red by -180, green by -170, and blue by -160, the three color components separate slightly along high-curvature borders. When additive-blended back together, the center stays pure white/neutral, while the outer bevel exhibits delicate prismatic rainbows when scrolling past headings and high-contrast elements.


5. Physical Depth & Hairline Highlights

The SVG filter takes care of light refraction, but we also need the tactile surface qualities of physical glass:

.glass {
  position: relative;
  width: fit-content;
  max-width: calc(100vw - 32px);
  height: 48px;
  display: flex;
  align-items: center;
  border-radius: 20px;
  
  /* Tint to ground the element on dark backgrounds */
  background: rgba(0, 0, 0, 0.2);
  
  /* Apply our custom optical filter */
  -webkit-backdrop-filter: url(#glass-filter) saturate(1);
  backdrop-filter: url(#glass-filter) saturate(1);
  
  /* Multi-tier shadows: hairline bevel + soft ambient occlusion */
  box-shadow: 
    rgba(255, 255, 255, 0.1) 0 0 0 0.5px inset,
    rgba(0, 0, 0, 0.1) 0 4px 16px,
    rgba(0, 0, 0, 0.08) 0 8px 24px;
}

The secret sauce here is box-shadow: rgba(255, 255, 255, 0.1) 0 0 0 0.5px inset. A 0.5px inset border catches imaginary light, creating the impression of a polished, rounded bevel edge without relying on thick, clunky border outlines.


6. GPU Compositing & Performance

Applying real-time displacement maps to a backdrop filter on every frame can be computationally demanding if the browser attempts CPU software rendering.

To guarantee a smooth 60fps / 120fps scroll experience:

  1. GPU Layer Promotion: We apply transform: translateZ(0), will-change: backdrop-filter, transform, and backface-visibility: hidden. This forces the compositor to place the navbar on its own GPU texture.
  2. Strict Paint Containment: contain: layout style paint informs the browser layout engine that internal layout recalculations inside the navbar do not dirty outside nodes.
  3. Hidden SVG Definition: The SVG hosting the filter definition is placed offscreen with pointer-events: none; opacity: 0; z-index: -10; transform: translateZ(0); so it never participates in hit-testing or layout passes.
  4. Accessible Fallbacks: For users with reduced motion preferences, CSS transitions are silenced (@media (prefers-reduced-motion: reduce)), and for mobile screens below 620px, we switch to a simplified compact navigation pattern to save GPU bandwidth.

The Takeaway

Digital design often flattens physical materials into single-property shortcuts. But by combining SVG filter primitives—which have existed in browsers for over two decades—with modern CSS backdrop filters and reactive component sizing, we can build interface surfaces that feel genuinely tangible.

csssvgsvelteoptics