Pure CSS Scenes & objects #hover

Hover tiles in pure CSS

Sweep across the floor. Tiles rise instantly and sink slowly, leaving a trail.

  • 40 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. The browser hit-tests in 3D: :hover works on the tile you actually see under the pointer, even on a tilted plane.
  2. The hovered cell itself never moves — only its ::before plate lifts. If the hovered element moved, it would slide out from under the pointer and flicker.
  3. The trail effect is two transition speeds. The base rule has a slow 1.4s transition — that one applies when the hover ends.
  4. The :hover rule overrides transition-duration to 0.08s — that one applies when the hover starts.
  5. Result: tiles pop up instantly and sink back slowly.

Key techniques

  • hit-testing works in 3D
  • fast transition in, slow transition out
  • isometric floor

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="floor">
    <i></i><i></i><i></i><i></i><i></i>
    <i></i><i></i><i></i><i></i><i></i>
    <i></i><i></i><i></i><i></i><i></i>
    <i></i><i></i><i></i><i></i><i></i>
    <i></i><i></i><i></i><i></i><i></i>
  </div>
</div>

CSS 40 lines

.scene {
  perspective: 900px;
}

.floor {
  display: grid;
  grid-template-columns: repeat(5, 46px);
  gap: 5px;
  transform-style: preserve-3d;
  transform: rotateX(56deg) rotateZ(-45deg);
  /* same plane as its cells: keep the floor itself out of hit-testing, or hover misses in patches */
  pointer-events: none;
}

/* the cell is a fixed hit target; only its ::before plate moves */
.floor i {
  pointer-events: auto;
  position: relative;
  height: 46px;
  transform-style: preserve-3d;
}

.floor i::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 7px;
  background: rgb(139 108 255 / 0.35);
  border: 1px solid rgb(139 108 255 / 0.7);
  pointer-events: none;
  /* slow on the way down */
  transition: transform 1.4s ease-out, background 1.4s ease-out;
}

.floor i:hover::before {
  background: #2ee6d6;
  transform: translateZ(46px);
  /* instant on the way up */
  transition-duration: 0.08s;
}

Sass source 42 lines

.d-isotiles {
  display: grid;
  grid-template-columns: repeat(5, 34px);
  gap: 4px;
  transform-style: preserve-3d;
  transform: rotateX(56deg) rotateZ(-45deg);
  // The container lies on exactly the same 3D plane as its children. Coplanar surfaces have no
  // stable front-to-back order, so in patches the browser hit-tests the (invisible) container
  // instead of the child: wrong cursor, ignored hover. Only the children should catch the pointer.
  pointer-events: none;

  // The cell never moves: it is the hit target. If the hovered element itself lifted, it would
  // slide out from under the pointer, lose :hover, drop, regain it... and flicker.
  i {
    position: relative;
    height: 34px;
    pointer-events: auto;
    transform-style: preserve-3d;

    // the visible plate
    &::before {
      content: '';
      position: absolute;
      inset: 0;
      border-radius: 6px;
      background: color-mix(in srgb, var(--accent) 35%, transparent);
      border: 1px solid color-mix(in srgb, var(--accent) 70%, transparent);
      pointer-events: none;
      // slow on the way DOWN (this rule applies once the hover ends)
      transition:
        transform 1.4s ease-out,
        background 1.4s ease-out;
    }

    &:hover::before {
      background: var(--accent-2);
      transform: translateZ(34px);
      // instant on the way UP
      transition-duration: 0.08s;
    }
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.