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.

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

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 dark gradient over each panel 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.

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 109 lines

.scene {
  perspective: 800px;
}

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

.fold:focus-visible {
  outline: 2px solid #2ee6d6;
  outline-offset: 2px;
}

/* every panel is a hinge that may hold the next panel; the visible paper is its <b> */
.panel {
  position: relative;
  width: 66px;
  height: 104px;
  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: 4px;
  width: 300%;
  height: 100%;
  background: linear-gradient(115deg, #8b6cff, #ff4d9d 60%, #ffb547);
  color: #fff;
  font-size: 41px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.03em;
  text-align: center;
}

.panel > b > span small {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.3em;
  opacity: 0.85;
}

/* 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.22), rgb(255 255 255 / 0));
}

.fold:hover .panel > b::after,
.fold:focus-visible .panel > b::after {
  opacity: 0;
}

Sass source 113 lines

$pw: 66px; // one panel
$ph: 104px;
$a: 52deg; // fold angle

// The static hit area: it takes :hover / :focus and never moves (the paper inside does).
.d-foldtext {
  display: grid;
  place-items: center;
  width: $pw * 3 + 16px;
  height: $ph + 40px;
  border-radius: 12px;
  cursor: pointer;
  transform-style: preserve-3d;

  &:focus-visible {
    outline: 2px solid var(--accent-2);
    outline-offset: 2px;
  }

  // Every panel is a "hinge" box that may hold the next panel; the visible paper is its <b>.
  // Hinges must not clip (that would flatten the panels hanging off them), the paper may.
  &__panel {
    position: relative;
    width: $pw;
    height: $ph;
    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($a);
  }

  // ...and its neighbours hang on its left and right edges and swing back twice as far, so
  // in absolute terms they end up at -$a: a Z seen from above.
  &__panel--left,
  &__panel--right {
    position: absolute;
    top: 0;
    transform: rotateY($a * -2);
  }

  &__panel--left {
    right: 100%;
    transform-origin: 100% 50%;
  }

  &__panel--right {
    left: 100%;
    transform-origin: 0 50%;
  }

  &:hover &__panel,
  &: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. All three show the same
    // sheet, each its own third, so the print lines up across the folds.
    > span {
      position: absolute;
      top: 0;
      left: calc(var(--i) * -100%);
      display: grid;
      place-content: center;
      gap: 4px;
      width: 300%;
      height: 100%;
      background: linear-gradient(115deg, var(--accent), var(--hot) 60%, var(--warm));
      color: #fff;
      font-size: 41px;
      font-weight: 900;
      line-height: 1;
      letter-spacing: 0.03em;
      text-align: center;

      small {
        font-size: 10px;
        font-weight: 700;
        letter-spacing: 0.3em;
        opacity: 0.85;
      }
    }

    // Shading that sells the fold: panels facing away from the light are darker. Fades out
    // as the paper flattens (opacity is cheap to animate, a gradient change is not).
    &::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.22), rgb(255 255 255 / 0));
  }

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