---
name: floating-panels
description: Use when you want "Premium, app-like, polished" - Makes interface blocks appear suspended above the page with shadows and motion.
---

# Floating Panels

> **Category:** Glass / Depth  -  **Personality:** Premium, app-like, polished
>
> **Best use:** Dashboards, SaaS landing pages
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

Floating Panels makes UI blocks read like physical cards hovering above the page. The lift comes almost entirely from **layered drop-shadows** — a tight ambient shadow plus a deep, soft cast shadow — that imply height, reinforced by gentle **motion**: a slow vertical bob and a pointer-driven 3D tilt. Because each panel sits at a different `translateZ` depth inside one `perspective` scene, tilting the stack parallaxes near panels more than far ones, so the composition reads as real stacked layers rather than a flat screenshot. It's a staple of premium SaaS heroes and dashboards where the product UI should feel light, tactile and alive.

## Dependencies / CDN

**None - pure CSS + a few lines of vanilla JS.** The demo only loads two display/body fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Sora:wght@600;700;800&display=swap" rel="stylesheet">
```

## HTML

A `perspective` scene wraps a `preserve-3d` cluster. One panel stays in flow (anchors the height); the rest are absolutely positioned around it. Each floating item is **three nested layers**: depth (`--z`) → bob → the visible panel. A blurred ellipse plays the contact shadow.

```html
<div class="fp-scene">
  <div class="fp-cluster">
    <span class="fp-ground" aria-hidden="true"></span>

    <!-- main panel: in flow, sits on the base plane -->
    <div class="fp-float fp-main" style="--z:0px">
      <div class="fp-bob" style="--dur:7s;--amp:-12px">
        <div class="fp-panel fp-elev-2 fp-card-main"> … dashboard … </div>
      </div>
    </div>

    <!-- floating accent: nearer the viewer, floats higher/faster -->
    <div class="fp-float fp-kpi" style="--z:55px">
      <div class="fp-bob" style="--dur:5.4s;--amp:-18px;--delay:-2s">
        <div class="fp-panel fp-elev-3 fp-card-kpi"> … KPI … </div>
      </div>
    </div>
  </div>
</div>
```

## CSS

```css
/* 1) THE STAGE — light, so shadows are actually visible */
.fp-scene{position:relative; perspective:1100px; padding:30px 8px}
.fp-cluster{position:relative; width:min(384px,100%); margin:0 auto;
  transform-style:preserve-3d;                 /* keep children in 3D space */
  transform:rotateX(5deg) rotateY(-11deg);     /* resting "product-shot" tilt */
  transition:transform .5s cubic-bezier(.2,.7,.2,1); will-change:transform}

/* 2) DEPTH + MOTION — split across nested elements so transforms never clash */
.fp-float{position:absolute; transform:translateZ(var(--z,0px))}  /* depth → parallax */
.fp-main{position:relative}                                       /* in flow, anchors height */
.fp-bob{animation:fp-bob var(--dur,7s) ease-in-out var(--delay,0s) infinite}
@keyframes fp-bob{0%,100%{transform:translateY(0)} 50%{transform:translateY(var(--amp,-12px))}}

/* 3) THE EFFECT — layered elevation shadows. Higher tier = floats higher. */
.fp-panel{background:linear-gradient(180deg,#fff,#fbfbff);
  border:1px solid rgba(40,42,90,.07); border-radius:20px;
  transition:box-shadow .3s, transform .3s}
.fp-elev-2{box-shadow:
  0 1px 2px rgba(30,32,68,.05), 0 3px 8px rgba(30,32,68,.06),
  0 12px 24px rgba(30,32,68,.07), 0 26px 46px -14px rgba(46,32,104,.2),
  inset 0 1px 0 rgba(255,255,255,.9)}                 /* lit top edge */
.fp-elev-3{box-shadow:
  0 2px 5px rgba(30,32,68,.06), 0 10px 22px rgba(30,32,68,.09),
  0 32px 56px -16px rgba(56,38,120,.26),
  inset 0 1px 0 rgba(255,255,255,.95)}
.fp-float:hover .fp-panel{transform:scale(1.025);   /* "pick up" the card */
  box-shadow:0 4px 10px rgba(30,32,68,.08),0 18px 32px rgba(30,32,68,.1),
             0 44px 72px -18px rgba(56,38,120,.34),inset 0 1px 0 rgba(255,255,255,.95)}

/* 4) CONTACT SHADOW — tightens + fades as the panel rises (synced to its bob) */
.fp-ground{position:absolute; left:50%; bottom:-30px; width:78%; height:30px;
  border-radius:50%; z-index:-1; filter:blur(9px);
  background:radial-gradient(50% 50% at 50% 50%,rgba(46,32,104,.3),transparent 72%);
  transform:translate(-50%,0) translateZ(-30px); animation:fp-ground 7s ease-in-out infinite}
@keyframes fp-ground{
  0%,100%{opacity:.5; transform:translate(-50%,0) translateZ(-30px) scaleX(1)}
  50%    {opacity:.34;transform:translate(-50%,0) translateZ(-30px) scaleX(.84)}}

@media (prefers-reduced-motion: reduce){
  .fp-bob,.fp-ground{animation:none}
  .fp-cluster{transition:none}
}
```

## JavaScript

Optional — the bob, tilt and parallax all run on CSS alone. This only adds **pointer-reactive tilt**; the per-panel parallax falls out for free because each panel sits at a different `translateZ`, so rotating the cluster shifts near panels more than far ones. Bail out for reduced-motion and touch.

```js
(function(){
  var stage = document.querySelector('.fp-stage'),     // pointer-tracking area
      cluster = document.querySelector('.fp-cluster');
  if(!stage || !cluster) return;
  var mq = window.matchMedia;
  if(mq && (mq('(prefers-reduced-motion: reduce)').matches || mq('(pointer: coarse)').matches)) return;

  var baseX = 5, baseY = -11, mx = 0, my = 0, raf = 0;
  function apply(){
    raf = 0;
    cluster.style.transform =
      'rotateX(' + (baseX - my*9).toFixed(2) + 'deg) ' +
      'rotateY(' + (baseY + mx*16).toFixed(2) + 'deg)';
  }
  stage.addEventListener('pointermove', function(e){
    var r = stage.getBoundingClientRect();
    mx = (e.clientX - r.left)/r.width  - .5;
    my = (e.clientY - r.top )/r.height - .5;
    if(!raf) raf = requestAnimationFrame(apply);   // throttle to one update per frame
  });
  stage.addEventListener('pointerleave', function(){
    cluster.style.transform = '';                  // CSS transition eases back to rest
  });
})();
```

## How it works

- **Layered `box-shadow` = the whole illusion.** Real depth needs *two or more* stacked shadows: a small, near-opaque one for the close ambient contact, and a large, blurred, low-alpha one offset downward for the cast shadow. A single shadow always looks flat/glued-on. Bumping the blur/offset/spread between `fp-elev-1/2/3` tiers is literally "how high it floats".
- **`inset 0 1px 0 rgba(255,255,255,.9)`** paints a lit top edge, so the panel catches light like a physical slab.
- **`perspective` + `translateZ` = automatic parallax.** Panels live in one `preserve-3d` cluster at different Z depths. When JS rotates the cluster a few degrees, perspective makes nearer (higher-Z) panels travel further across the screen than far ones — depth for free, no per-element math.
- **Motion is split across nested elements on purpose:** depth (`translateZ`) on `.fp-float`, the bob (`translateY` keyframe) on `.fp-bob`, the hover lift (`scale`) on `.fp-panel`. Each owns its own `transform` so they compose instead of overwriting each other.
- **The contact-shadow ellipse** shares the panel's bob duration but animates *inversely* (smaller + lighter at the top of the rise) — the cue your eye reads as "this thing left the ground".

## Customization

- **Float height:** swap the shadow tier (`fp-elev-1` → `fp-elev-3`) *and* the bob `--amp`/`--dur`. Keep them correlated — a high shadow with a tiny bob feels wrong.
- **Tilt strength:** the `*9` / `*16` multipliers in JS and the resting `rotateX/rotateY`. More perspective punch = lower `perspective` value (e.g. `900px`); flatter = `1600px`.
- **Desync the stack:** give each panel its own `--dur` and a negative `--delay` so they bob out of phase (a uniform bob looks mechanical).
- **Palette / surface:** shadows must sit on a *light* surface. Re-tint the shadow rgba toward your brand hue (here a faint violet `rgba(46,32,104,…)`) instead of pure black for a richer feel.

## Accessibility & performance

- **Honour `prefers-reduced-motion`:** kill the bob/ground keyframes and the tilt transition, and `return` early in JS before binding listeners.
- **Skip touch (`pointer: coarse`)** — a hover/tilt the user can't drive just causes jumps; let it rest at the static tilt.
- **Animate only `transform`/`opacity`** (GPU-cheap). Never animate `box-shadow` per-frame — transition it on hover only, as here.
- **Throttle pointer work** through `requestAnimationFrame` (one transform write per frame), and keep the number of simultaneously-shadowed panels modest.

## Gotchas

- **Shadows are invisible on dark/flat backgrounds.** This effect needs a light, slightly varied surface to read as "floating" — on near-black it just looks like solid cards. (That's also what sets it apart from Glassmorphism, which needs the *opposite*.)
- **One `transform` per element.** Putting the bob and the parallax on the *same* node makes one silently overwrite the other — that's why depth, bob and hover live on three nested elements.
- **`translateZ` does nothing without a `perspective` ancestor**, and the depth is only honoured if the *parent* is `transform-style: preserve-3d`.
- **Don't flatten the 3D.** Setting `overflow` (other than `visible`) or a `filter` on the element *between* the `perspective` node and the `preserve-3d` cluster collapses everything to 2D and kills the parallax. Clip at an outer wrapper instead.
- **Keep the tilt subtle** (a few degrees). Past ~12° the panels skew and the dashboard text gets hard to read.
