Pure CSS Products & branding #hover #loop #product #mockup #ui #isometric

Layered browser in pure CSS

A UI mockup cut into depth layers: window, sidebar, cards and a floating button each sit at their own translateZ. Hover or focus pulls them further apart.

  • 191 lines of CSS
  • 13 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The window is tilted like an isometric drawing: rotateX(55deg) rotateZ(-38deg). Its layers are flat siblings inside it, each lifted along the window’s own Z axis.
  2. Each layer declares its depth once in the markup (style="--z:40") and shares a single rule: translateZ(calc(var(--z) * var(--k) * var(--u))), where --u is the one base unit every length here is a multiple of, so the window is the same share of a gallery card, the editor and a recording canvas.
  3. Hover changes only the multiplier --k on the wrapper (0.45 → 1). Every layer’s transform changes with it and its own transition animates it; a delay of --z × 2ms lets the top layers rise last.
  4. The bob sits on the window, the hover on a static wrapper, and the window has pointer-events: none, so the bobbing never slides out from under the pointer.

Key techniques

  • rotateX + rotateZ isometric tilt
  • translateZ(calc(var(--z) * var(--k) * var(--u)))
  • one multiplier changes on :hover
  • static hit area, bobbing child

Copy-paste code

HTML 13 lines

<div class="scene">
  <div class="browser" tabindex="0" role="img" aria-label="Browser window whose layers lift apart on hover or focus">
    <div class="win">
      <i class="shadow"></i>
      <div class="frame"><span></span><span></span><span></span><em></em></div>
      <div class="side" style="--z:14"><i></i><i></i><i></i><i></i></div>
      <i class="card hero" style="--z:26"></i>
      <i class="card a" style="--z:40"></i>
      <i class="card b" style="--z:40"></i>
      <b class="fab" style="--z:58"></b>
    </div>
  </div>
</div>

CSS 191 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the window is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.47vmin;
  perspective: calc(800 * var(--u));
}

/* static hit area: it only changes the multiplier --k */
.browser {
  --k: 0.45;
  display: grid;
  place-items: center;
  width: calc(210 * var(--u));
  height: calc(184 * var(--u));
  border-radius: calc(16 * var(--u));
  outline-offset: calc(-4 * var(--u));
  cursor: pointer;
  transform-style: preserve-3d;
}

.browser:hover,
.browser:focus-visible {
  --k: 1;
}

/* 152 × 112, tilted and bobbing */
.win {
  position: relative;
  width: calc(152 * var(--u));
  height: calc(112 * var(--u));
  pointer-events: none;
  transform-style: preserve-3d;
  animation: bob 3.2s ease-in-out infinite alternate;
}

/* every depth layer: one transform from --z × --k, in base units, staggered by depth */
.win > [style] {
  position: absolute;
  box-sizing: border-box;
  transform: translateZ(calc(var(--z) * var(--k) * var(--u)));
  transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
  transition-delay: calc(var(--z) * 2ms);
}

.shadow {
  position: absolute;
  inset: calc(4 * var(--u)) calc(-6 * var(--u)) calc(-8 * var(--u)) calc(4 * var(--u));
  border-radius: calc(14 * var(--u));
  background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
  transform: translateZ(calc(var(--k) * -24 * var(--u))); /* sinks as the layers rise */
  transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
}

/* the window at z = 0: title bar with traffic lights and an address bar */
.frame {
  position: absolute;
  inset: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: flex-start;
  gap: calc(3 * var(--u));
  padding: calc(5 * var(--u)) calc(6 * var(--u));
  border: calc(1 * var(--u)) solid rgb(140 150 220 / 0.34);
  border-radius: calc(10 * var(--u));
  background:
    linear-gradient(#2a2d44 0 calc(16 * var(--u)), transparent calc(16 * var(--u))),
    #222249;
}

.frame span {
  width: calc(6 * var(--u));
  height: calc(6 * var(--u));
  border-radius: 50%;
  background: #ff4d9d;
}

.frame span:nth-child(2) { background: #ffb547; }
.frame span:nth-child(3) { background: #2ee6d6; }

.frame em {
  width: calc(62 * var(--u));
  height: calc(7 * var(--u));
  margin-left: calc(14 * var(--u));
  border-radius: calc(4 * var(--u));
  background: rgb(236 238 251 / 0.16);
}

.side {
  top: calc(23 * var(--u));
  left: calc(7 * var(--u));
  display: grid;
  align-content: start;
  gap: calc(6 * var(--u));
  width: calc(30 * var(--u));
  height: calc(82 * var(--u));
  padding: calc(6 * var(--u)) calc(5 * var(--u));
  border: calc(1 * var(--u)) solid rgb(139 108 255 / 0.45);
  border-radius: calc(6 * var(--u));
  background: #2e2a5e;
}

.side i { height: calc(4 * var(--u)); border-radius: calc(2 * var(--u)); background: rgb(236 238 251 / 0.3); }
.side i:first-child { background: #8b6cff; }
.side i:nth-child(3) { width: 70%; }

.card {
  border: calc(1 * var(--u)) solid rgb(255 255 255 / 0.35);
  border-radius: calc(6 * var(--u));
}

/* hero banner with two lines of "text" */
.hero {
  top: calc(23 * var(--u));
  left: calc(44 * var(--u));
  width: calc(101 * var(--u));
  height: calc(32 * var(--u));
  background:
    linear-gradient(rgb(255 255 255 / 0.9) 0 0) calc(8 * var(--u)) calc(9 * var(--u)) / 42% calc(4 * var(--u)) no-repeat,
    linear-gradient(rgb(255 255 255 / 0.55) 0 0) calc(8 * var(--u)) calc(17 * var(--u)) / 28% calc(3 * var(--u)) no-repeat,
    linear-gradient(120deg, #8b6cff, #ff4d9d);
}

.a,
.b {
  top: calc(62 * var(--u));
  width: calc(47 * var(--u));
  height: calc(43 * var(--u));
}

/* mini bar chart */
.a {
  left: calc(44 * var(--u));
  padding: calc(7 * var(--u)) 0 calc(5 * var(--u));
  background:
    linear-gradient(0deg, #2ee6d6 0 45%, transparent 45%) calc(7 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 70%, transparent 70%) calc(15 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 55%, transparent 55%) calc(23 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 85%, transparent 85%) calc(31 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
    #193d4e;
  background-origin: content-box;
}

/* donut: a conic square rounded off by two radial layers in the card colour */
.b {
  --paper: #3e3434;
  right: calc(7 * var(--u));
  background:
    radial-gradient(circle, var(--paper) 0 calc(6 * var(--u)), transparent calc(6.5 * var(--u)) calc(11 * var(--u)), var(--paper) calc(11.5 * var(--u))),
    conic-gradient(#ffb547 0 70%, #785b3a 0) 50% 50% / calc(24 * var(--u)) calc(24 * var(--u)) no-repeat,
    var(--paper);
}

/* the floating button; rotate(38deg) undoes the window's rotateZ so "+" stays a plus */
.win > .fab {
  right: calc(12 * var(--u));
  bottom: calc(12 * var(--u));
  display: grid;
  place-items: center;
  width: calc(22 * var(--u));
  height: calc(22 * var(--u));
  border-radius: 50%;
  background: linear-gradient(135deg, #ff4d9d, #8b6cff);
  box-shadow: 0 calc(2 * var(--u)) calc(6 * var(--u)) rgb(0 0 0 / 0.35);
  transform: translateZ(calc(var(--z) * var(--k) * var(--u))) rotate(38deg);
}

/* the "+" is two bars in one grid cell: exactly centred, unlike a text glyph */
.win > .fab::before,
.win > .fab::after {
  content: '';
  grid-area: 1 / 1;
  width: calc(10 * var(--u));
  height: calc(2 * var(--u));
  border-radius: calc(1 * var(--u));
  background: #fff;
}

.win > .fab::after {
  rotate: 90deg;
}

/* translateY first: the bob is straight up and down on screen. Measured by what it paints, the
   window's solid body already sits a little low in its layout box, and hovering lifts the
   layers up the screen (they rise towards the viewer, which is up the screen at this tilt), so
   the bob runs from the box to 8 units above it: the paused window, at the bottom of the swing,
   is centred, and so is the whole swing, raised layers and all. */
@keyframes bob {
  from { transform: translateY(0) rotateX(55deg) rotateZ(-38deg); }
  to   { transform: translateY(calc(-8 * var(--u))) rotateX(55deg) rotateZ(-38deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.