Pure CSS Text effects #text #loop #shape

Rotating word prism in pure CSS

Four words on the four long sides of a prism that turns a quarter at a time, pausing on each. After 360° it is back where it started, so the loop has no seam.

  • 78 lines of CSS
  • 11 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Face n is turned n quarter turns backwards around X, then pushed out with translateZ by half the prism’s depth — the same "turn, then push" recipe as a basic cube, just on one axis.
  2. The whole prism is pulled back by half its depth (translateZ(calc(-18 * var(--u)))) so the face currently in front sits exactly at z = 0 and stays crisp. --u is the one base unit every length here is a multiple of.
  3. The keyframes hold on each face for a stretch, then turn 90deg with their own cubic-bezier — a snappy, springy turn rather than a constant spin.
  4. backface-visibility: hidden stops a word from showing mirrored through the box while it turns.
  5. 360deg looks exactly like 0deg, so the loop has no seam.
  6. Mid-turn the prism stands on an edge and sweeps √2 × 36 units tall, half of that above the resting face and half below. That corner, not the face, is what the band has to hold.

Key techniques

  • rotateX(n × -90deg) translateZ(h / 2)
  • hold + turn keyframes
  • backface-visibility: hidden
  • per-keyframe easing

Copy-paste code

HTML 11 lines

<div class="scene">
  <div class="wordcube">
    <span>We make</span>
    <span class="prism">
      <span style="--i:0;--c:#8b6cff">design</span>
      <span style="--i:1;--c:#2ee6d6">code</span>
      <span style="--i:2;--c:#ff4d9d">motion</span>
      <span style="--i:3;--c:#ffb547">brands</span>
    </span>
  </div>
</div>

CSS 78 lines

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

/* The lead-in sits over the prism, not beside it: side by side the line is five times wider
   than it is tall, so at the 92vmin width limit it stands 18vmin high, well under the 40vmin
   floor. Stacked, it is the shape of the headline it is imitating anyway. */
.wordcube {
  display: grid;
  justify-items: center;
  gap: calc(9 * var(--u));
  /* no colour of its own: the lead-in is written on the stage, so it takes the stage's ink */
  font-size: calc(21 * var(--u));
  font-weight: 800;
  white-space: nowrap;
  transform-style: preserve-3d;
  /* measured by what it paints, the turning prism reaches further below the line than the lead-in
     does above it, so the drawing hangs below the layout box; this lift centres it */
  transform: translateY(calc(-5 * var(--u)));
}

.prism {
  position: relative;
  display: inline-block;
  width: calc(104 * var(--u));
  height: calc(36 * var(--u)); /* face height = prism depth: its cross-section is a square */
  transform-style: preserve-3d;
  animation: cube-turn 9s infinite;
}

.prism span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  padding-left: calc(13 * var(--u));
  border-radius: calc(8 * var(--u));
  background: color-mix(in srgb, var(--c) 26%, transparent);
  /* the edge is an inner line, not a border: turning, a face passes side-on, where a hairline
     border breaks up; the inner glow is the soft shade that survives */
  box-shadow:
    inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, var(--c) 80%, transparent),
    inset 0 0 calc(18 * var(--u)) color-mix(in srgb, var(--c) 32%, transparent);
  /* the face is see-through, so the word is really on the stage: its colour leans towards the
     stage's ink, lighter on a dark stage and darker on a light one, so it reads on both */
  color: color-mix(in srgb, var(--c) 55%, currentColor);
  backface-visibility: hidden;
  transform: rotateX(calc(var(--i) * -90deg)) translateZ(calc(18 * var(--u)));
}

@keyframes cube-turn {
  0%, 19% {
    transform: translateZ(calc(-18 * var(--u))) rotateX(0deg);
    animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
  }

  25%, 44% {
    transform: translateZ(calc(-18 * var(--u))) rotateX(90deg);
    animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
  }

  50%, 69% {
    transform: translateZ(calc(-18 * var(--u))) rotateX(180deg);
    animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
  }

  75%, 94% {
    transform: translateZ(calc(-18 * var(--u))) rotateX(270deg);
    animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
  }

  100% {
    transform: translateZ(calc(-18 * var(--u))) rotateX(360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.