Pure CSS Text effects #text #hover #paper

Z-fold headline in pure CSS

A headline printed across three panels folded like a brochure. Each panel is a window onto the same wide sheet; hover or focus flattens the paper.

  • 112 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Panels are nested inside a fixed "hinge": the middle panel swings rotateY(52deg), and its left/right neighbours hang off its edges and swing back twice as far — so in absolute terms they land at -52deg, folding like a “Z” seen from above.
  2. Every hinge needs transform-style: preserve-3d or the fold flattens at that level; the visible paper inside each hinge is a separate element that is allowed to clip.
  3. All three panels are windows onto the same sheet, three panels wide, each shifted left by calc(var(--i) * -100%) — so the printed headline lines up across the folds.
  4. A shading gradient over each panel (dark on the outer two, a light sheen on the middle one) fades to opacity: 0 on hover/focus — cheap, since only opacity is animating, not the gradient itself.
  5. On :hover/:focus-visible every panel’s transform resets to none, flattening the whole sheet in one shared transition.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the headline is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • nested hinges with transform-origin
  • same sheet offset by --i × -100%
  • clip the leaf, never the hinge
  • shading faded with opacity

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="fold" tabindex="0" aria-label="Unfold: a headline on folded paper, flattens on hover or focus">
    <div class="panel panel-mid">
      <b style="--i:1" aria-hidden="true"><span><small>HOVER TO</small>UNFOLD</span></b>
      <div class="panel panel-left"><b style="--i:0" aria-hidden="true"><span><small>HOVER TO</small>UNFOLD</span></b></div>
      <div class="panel panel-right"><b style="--i:2" aria-hidden="true"><span><small>HOVER TO</small>UNFOLD</span></b></div>
    </div>
  </div>
</div>

CSS 112 lines

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

/* the static hit area: it takes :hover / :focus and never moves (the paper inside does) */
.fold {
  display: grid;
  place-items: center;
  width: calc(214 * var(--u)); /* 3 panels x 66 units, plus a little breathing room */
  height: calc(144 * var(--u));
  border-radius: calc(12 * var(--u));
  cursor: pointer;
  transform-style: preserve-3d;
}

.fold:focus-visible {
  outline: calc(2 * var(--u)) solid #2ee6d6;
  outline-offset: calc(2 * var(--u));
}

/* every panel is a hinge that may hold the next panel; the visible paper is its <b> */
.panel {
  position: relative;
  width: calc(66 * var(--u));
  height: calc(104 * var(--u));
  pointer-events: none;
  transform-style: preserve-3d;
  transition: transform 0.9s cubic-bezier(0.3, 1.25, 0.5, 1);
}

/* the middle panel is the root: it swings one way... */
.panel-mid {
  transform: rotateX(8deg) rotateY(52deg);
}

/* ...and its neighbours hang on its edges and swing back twice as far */
.panel-left,
.panel-right {
  position: absolute;
  top: 0;
  transform: rotateY(-104deg);
}

.panel-left {
  right: 100%;
  transform-origin: 100% 50%;
}

.panel-right {
  left: 100%;
  transform-origin: 0 50%;
}

.fold:hover .panel,
.fold:focus-visible .panel {
  transform: none;
}

/* the paper: a window one panel wide... */
.panel > b {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
}

/* ...onto a sheet three panels wide, shifted left by --i panels, so the same sheet lines up */
.panel > b > span {
  position: absolute;
  top: 0;
  left: calc(var(--i) * -100%);
  display: grid;
  place-content: center;
  gap: calc(4 * var(--u));
  width: 300%;
  height: 100%;
  /* deep enough for its white words: the bright #8b6cff, #ff4d9d, #ffb547 were 2.5:1 */
  background: linear-gradient(115deg, #6a45f5, #d1206f 60%, #c96f00);
  color: #fff;
  font-size: calc(41 * var(--u));
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.03em;
  text-align: center;
}

.panel > b > span small {
  font-size: calc(10 * var(--u));
  font-weight: 700;
  letter-spacing: 0.3em; /* full white: at 0.85, under the middle panel's sheen, it was 2.9:1 */
}

/* shading that sells the fold: fades out as the paper flattens (opacity is cheap to animate) */
.panel > b::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgb(0 0 0 / 0.45), rgb(0 0 0 / 0.1));
  transition: opacity 0.9s;
}

.panel-mid > b::after {
  background: linear-gradient(90deg, rgb(255 255 255 / 0.12), rgb(255 255 255 / 0));
}

.fold:hover .panel > b::after,
.fold:focus-visible .panel > b::after {
  opacity: 0;
}
Standalone snippet · plain CSS, no build step, no dependencies.