Pure CSS Cards & galleries #hover #cards #grid #ui

Flip tile grid in pure CSS

Six tiles that lift and flip to show their back. Odd tiles turn sideways, even ones head over heels, from a single custom property holding the rotation.

  • 76 lines of CSS
  • 22 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The tile never moves — it is the hit target. Only the .tile-inner wrapper inside it flips, on :hover / :focus-visible.
  2. One custom property, --flip, holds the whole rotation function (rotateY(180deg) or rotateX(180deg)), set once per tile so a single rule can flip every tile differently.
  3. The back face is pre-rotated by that same --flip in its resting state, so once the tile turns, the back lands right-way up instead of mirrored.
  4. backface-visibility: hidden on both faces means only the one currently facing you is ever visible.
  5. Lifting with translateZ before the flip keeps the tile from clipping into its neighbours mid-turn.
  6. Every length is a multiple of one base unit, --u, so the grid is the same share of a gallery card, the editor and a recording canvas. The lifted tile is the widest the model gets, because the perspective magnifies whatever comes toward you, so that is the pose the band is measured against.

Key techniques

  • a transform function stored in --flip
  • static tile, flipping inner
  • container pointer-events: none
  • backface-visibility: hidden

Copy-paste code

HTML 22 lines

<div class="scene">
  <div class="grid">
    <div class="tile" tabindex="0" style="--c:#8b6cff">
      <div class="tile-inner"><b>01</b><span>tilt</span></div>
    </div>
    <div class="tile" tabindex="0" style="--c:#ff4d9d">
      <div class="tile-inner"><b>02</b><span>flip</span></div>
    </div>
    <div class="tile" tabindex="0" style="--c:#2ee6d6;--on:#10131f">
      <div class="tile-inner"><b>03</b><span>spin</span></div>
    </div>
    <div class="tile" tabindex="0" style="--c:#ffb547;--on:#10131f">
      <div class="tile-inner"><b>04</b><span>lift</span></div>
    </div>
    <div class="tile" tabindex="0" style="--c:#8b6cff">
      <div class="tile-inner"><b>05</b><span>fold</span></div>
    </div>
    <div class="tile" tabindex="0" style="--c:#ff4d9d">
      <div class="tile-inner"><b>06</b><span>zoom</span></div>
    </div>
  </div>
</div>

CSS 76 lines

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

.grid {
  display: grid;
  grid-template-columns: repeat(3, calc(62 * var(--u)));
  gap: calc(9 * var(--u));
  transform-style: preserve-3d;
  transform: rotateX(16deg);
  /* the grid and its tiles are coplanar: take the grid out of hit-testing so the
     pointer always resolves to a tile, never something ambiguous between them */
  pointer-events: none;
}

.tile {
  --flip: rotateY(180deg); /* odd tiles flip sideways */
  position: relative;
  height: calc(62 * var(--u));
  outline: none;
  cursor: pointer;
  pointer-events: auto;
  transform-style: preserve-3d;
}

.tile:nth-child(even) {
  --flip: rotateX(180deg); /* even tiles flip head over heels */
}

.tile-inner {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform-style: preserve-3d;
  transition: transform 0.6s cubic-bezier(0.3, 1.25, 0.5, 1);
}

.tile-inner > * {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: calc(10 * var(--u));
  backface-visibility: hidden;
}

/* front */
/* the front is see-through, so its number is really on the stage: it takes the stage's ink */
.tile-inner b {
  font-size: calc(20 * var(--u));
  font-weight: 900;
  background: color-mix(in srgb, var(--c) 26%, transparent);
  border: calc(1 * var(--u)) solid color-mix(in srgb, var(--c) 75%, transparent);
  box-shadow: inset 0 0 calc(24 * var(--u)) color-mix(in srgb, var(--c) 30%, transparent);
}

/* back: pre-turned with the same rotation the tile will make, so it lands the right way up */
.tile-inner span {
  background: linear-gradient(135deg, var(--c), color-mix(in srgb, var(--c) 45%, #000));
  /* the back is solid, so its word is set for its own colour: white on violet and pink, dark
     on teal and amber, where white would not read */
  color: var(--on, #fff);
  font-size: calc(12 * var(--u));
  font-weight: 800;
  letter-spacing: 0.04em;
  transform: var(--flip);
}

.tile:hover .tile-inner,
.tile:focus-visible .tile-inner {
  /* lift first (toward the viewer), then flip in place */
  transform: translateZ(calc(30 * var(--u))) var(--flip);
}
Standalone snippet · plain CSS, no build step, no dependencies.