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.

  • 178 lines of CSS
  • 13 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

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) * 1px)).
  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) * 1px))
  • one multiplier changes on :hover
  • static hit area, bobbing child

Copy-paste code

HTML 13 lines

<div class="scene">
  <div class="browser" tabindex="0">
    <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 178 lines

.scene {
  perspective: 800px;
}

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

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

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

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

.shadow {
  position: absolute;
  inset: 4px -6px -8px 4px;
  border-radius: 14px;
  background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
  transform: translateZ(calc(var(--k) * -24px)); /* 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: 3px;
  padding: 5px 6px;
  border: 1px solid rgb(140 150 220 / 0.34);
  border-radius: 10px;
  background:
    linear-gradient(#2a2d44 0 16px, transparent 16px),
    #222249;
}

.frame span { width: 6px; height: 6px; border-radius: 50%; background: #ff4d9d; }
.frame span:nth-child(2) { background: #ffb547; }
.frame span:nth-child(3) { background: #2ee6d6; }

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

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

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

.card {
  border: 1px solid rgb(255 255 255 / 0.35);
  border-radius: 6px;
}

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

.a,
.b {
  top: 62px;
  width: 47px;
  height: 43px;
}

/* mini bar chart */
.a {
  left: 44px;
  padding: 7px 0 5px;
  background:
    linear-gradient(0deg, #2ee6d6 0 45%, transparent 45%) 7px 100% / 5px 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 70%, transparent 70%) 15px 100% / 5px 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 55%, transparent 55%) 23px 100% / 5px 100% no-repeat,
    linear-gradient(0deg, #2ee6d6 0 85%, transparent 85%) 31px 100% / 5px 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: 7px;
  background:
    radial-gradient(circle, var(--paper) 0 6px, transparent 6.5px 11px, var(--paper) 11.5px),
    conic-gradient(#ffb547 0 70%, #785b3a 0) 50% 50% / 24px 24px no-repeat,
    var(--paper);
}

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

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

/* translateY first: the bob is straight up and down on screen */
@keyframes bob {
  from { transform: translateY(8px) rotateX(55deg) rotateZ(-38deg); }
  to   { transform: translateY(0) rotateX(55deg) rotateZ(-38deg); }
}

Sass source 212 lines

$w: 152px;
$h: 112px;
$bar: 16px; // title bar

// Static hit area. It only changes one number, --k, and every layer multiplies its own depth
// (--z, set in the markup) by it.
.d-browser {
  --k: 0.45;
  display: grid;
  place-items: center;
  width: 210px;
  height: 184px;
  border-radius: 16px;
  outline-offset: -4px;
  cursor: pointer;
  transform-style: preserve-3d;

  &:hover,
  &:focus-visible {
    --k: 1;
  }

  // The tilted window. It bobs, the layers inside ride along.
  &__win {
    position: relative;
    width: $w;
    height: $h;
    pointer-events: none;
    transform-style: preserve-3d;
    animation: d-browser-bob 3.2s ease-in-out infinite alternate;

    // every depth layer: one transform, driven by --z × --k, staggered so the top rises last
    > [style] {
      position: absolute;
      transform: translateZ(calc(var(--z) * var(--k) * 1px));
      transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
      transition-delay: calc(var(--z) * 2ms);
    }
  }

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

  // the window itself, z = 0: glass panel, title bar with traffic lights and an address bar
  &__frame {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    align-content: flex-start;
    flex-wrap: wrap;
    gap: 3px;
    padding: 5px 6px;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    background:
      linear-gradient(color-mix(in srgb, var(--text) 10%, var(--surface-solid)) 0 $bar, transparent $bar),
      color-mix(in srgb, var(--surface-solid) 88%, var(--accent));

    span {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--hot);

      &:nth-child(2) {
        background: var(--warm);
      }

      &:nth-child(3) {
        background: var(--accent-2);
      }
    }

    em {
      width: 62px;
      height: 7px;
      margin-left: 14px;
      border-radius: 4px;
      background: color-mix(in srgb, var(--text) 16%, transparent);
    }
  }

  // sidebar with four nav rows, the first one "active"
  &__side {
    top: $bar + 7px;
    left: 7px;
    display: grid;
    align-content: start;
    gap: 6px;
    width: 30px;
    height: $h - $bar - 14px;
    padding: 6px 5px;
    border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
    border-radius: 6px;
    background: color-mix(in srgb, var(--accent) 22%, var(--surface-solid));

    i {
      height: 4px;
      border-radius: 2px;
      background: color-mix(in srgb, var(--text) 30%, transparent);

      &:first-child {
        background: var(--accent);
      }

      &:nth-child(3) {
        width: 70%;
      }
    }
  }

  &__card {
    border: 1px solid rgb(255 255 255 / 0.35);
    border-radius: 6px;

    // hero banner: gradient with two lines of "text"
    &--hero {
      top: $bar + 7px;
      left: 44px;
      width: $w - 51px;
      height: 32px;
      background:
        linear-gradient(rgb(255 255 255 / 0.9) 0 0) 8px 9px / 42% 4px no-repeat,
        linear-gradient(rgb(255 255 255 / 0.55) 0 0) 8px 17px / 28% 3px no-repeat,
        linear-gradient(120deg, var(--accent), var(--hot));
    }

    &--a,
    &--b {
      top: $bar + 46px;
      width: ($w - 58px) * 0.5;
      height: $h - $bar - 53px;
    }

    // mini bar chart
    &--a {
      left: 44px;
      background:
        linear-gradient(0deg, var(--accent-2) 0 45%, transparent 45%) 7px 100% / 5px 100% no-repeat,
        linear-gradient(0deg, var(--accent-2) 0 70%, transparent 70%) 15px 100% / 5px 100% no-repeat,
        linear-gradient(0deg, var(--accent-2) 0 55%, transparent 55%) 23px 100% / 5px 100% no-repeat,
        linear-gradient(0deg, var(--accent-2) 0 85%, transparent 85%) 31px 100% / 5px 100% no-repeat,
        color-mix(in srgb, var(--accent-2) 18%, var(--surface-solid));
      background-origin: content-box;
      padding: 7px 0 5px;
    }

    // donut stat: a conic square, rounded off by two radial layers in the card colour
    &--b {
      --paper: color-mix(in srgb, var(--warm) 18%, var(--surface-solid));
      right: 7px;
      background:
        radial-gradient(circle, var(--paper) 0 6px, transparent 6.5px 11px, var(--paper) 11.5px),
        conic-gradient(var(--warm) 0 70%, color-mix(in srgb, var(--warm) 30%, var(--paper)) 0) 50% 50% / 24px 24px no-repeat,
        var(--paper);
    }
  }

  // the floating action button, highest layer
  &__fab {
    right: 12px;
    bottom: 12px;
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--hot), var(--accent));
    box-shadow: 0 2px 6px rgb(0 0 0 / 0.35);

    // the "+" is two drawn bars in the same grid cell: exactly centred (a text glyph sits on the
    // font's baseline, so it never is)
    &::before,
    &::after {
      content: '';
      grid-area: 1 / 1;
      width: 10px;
      height: 2px;
      border-radius: 1px;
      background: #fff;
    }

    &::after {
      rotate: 90deg;
    }

    // same lift as every layer, then undo the window's rotateZ so the "+" does not read as "×"
    // ([style] matches the specificity of the layer rule above, and comes later)
    &[style] {
      transform: translateZ(calc(var(--z) * var(--k) * 1px)) rotate(38deg);
    }
  }
}

// translateY comes first, so the bob is straight up and down on screen, not along the tilted axes
@keyframes d-browser-bob {
  from {
    transform: translateY(8px) rotateX(55deg) rotateZ(-38deg);
  }

  to {
    transform: translateY(0) rotateX(55deg) rotateZ(-38deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.