10 CSS Parallax Effects 07 / 10

CSS Parallax Text Overlay Effect

Three typographic scenes demonstrating the fixed-text / sliding-texture trick.

CSS + JS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="plx-07">

  <!-- SCENE 1: Display headline / scrolling stripe bg -->
  <section class="plx-07__s1">
    <div class="plx-07__s1-pin">
      <div class="plx-07__s1-bg" id="plx07-s1-bg"></div>
      <div class="plx-07__s1-text">
        <span class="plx-07__display" id="plx07-line1">TYPE</span>
        <span class="plx-07__display is-outline" id="plx07-line2">OVER</span>
        <span class="plx-07__display is-accent" id="plx07-line3">IMAGE</span>
      </div>
      <p class="plx-07__s1-label">Background scrolls · Text stays</p>
    </div>
  </section>

  <!-- SCENE 2: Quote / bg moves faster than text -->
  <section class="plx-07__s2">
    <div class="plx-07__s2-pin">
      <div class="plx-07__s2-bg" id="plx07-s2-bg"></div>
      <div class="plx-07__quote-wrap" id="plx07-quote">
        <span class="plx-07__qmark">"</span>
        <p class="plx-07__qtext">
          Design is the art of making decisions <em>invisible.</em>
          The best interface is the one you never notice —
          only the <em>experience</em> remains.
          <span class="plx-07__qattr">— A Designer Who Believed It</span>
        </p>
      </div>
    </div>
  </section>

  <!-- SCENE 3: Split word / bg slides under it -->
  <section class="plx-07__s3">
    <div class="plx-07__s3-pin">
      <div class="plx-07__s3-bg" id="plx07-s3-bg"></div>
      <div class="plx-07__word-wrap" id="plx07-word">
        <span class="plx-07__word-solid">BOLD</span>
        <span class="plx-07__word-outline">BOLD</span>
      </div>
      <div class="plx-07__s3-caption">
        <h3>The texture moves;<br>the word doesn't.</h3>
        <p>Massive display type stays anchored while the rich background texture shifts beneath it. The contrast between stillness and motion creates instant depth.</p>
      </div>
    </div>
  </section>

  <div class="plx-07__footer">
    <h2>Words that <em>command</em> space.</h2>
    <p>The parallax text overlay technique uses motion differential between typography and background to create perceived depth on a flat screen.</p>
  </div>

</div>
.plx-07, .plx-07 *, .plx-07 *::before, .plx-07 *::after {
  box-sizing: border-box; margin: 0; padding: 0;
}
.plx-07 {
  font-family: 'DM Sans', sans-serif;
  background: #0c0c0a;
  color: #f4f0e8;
  /* overflow:clip not overflow:hidden — see Demo 09 + 10 fixes.
     overflow:hidden creates a scroll context that breaks
     position:sticky on descendants. */
  overflow-x: clip;
}

/* ═══ SCENE 1: Headline stays fixed, stripe bg scrolls beneath ═══ */
.plx-07__s1 {
  position: relative;
  /* 300vh = 200vh of sticky pin runway during which the JS
     animates bg + text overlays. Sticky pinning works because
     .plx-07 root uses overflow-x:clip (not :hidden) — the
     :hidden form would create a scroll context that breaks
     sticky on descendants. */
  height: 300vh;
}
.plx-07__s1-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Diagonal stripe background — this is what scrolls */
.plx-07__s1-bg {
  position: absolute;
  inset: -30%;
  will-change: transform;
  background:
    repeating-linear-gradient(
      -55deg,
      rgba(220,160,60,0.0) 0px,
      rgba(220,160,60,0.0) 30px,
      rgba(220,160,60,0.06) 30px,
      rgba(220,160,60,0.06) 32px,
      rgba(220,160,60,0.0) 32px,
      rgba(220,160,60,0.0) 62px,
      rgba(220,160,60,0.1) 62px,
      rgba(220,160,60,0.1) 64px
    ),
    radial-gradient(ellipse at 30% 50%, rgba(180,100,20,0.25) 0%, transparent 55%),
    radial-gradient(ellipse at 75% 30%, rgba(100,60,140,0.2) 0%, transparent 50%),
    linear-gradient(160deg, #1a1206 0%, #0c0c0a 60%, #060810 100%);
}

/* The text itself — stays put */
.plx-07__s1-text {
  position: relative;
  z-index: 5;
  text-align: center;
  mix-blend-mode: normal;
}

.plx-07__display {
  display: block;
  font-family: 'Anton', sans-serif;
  font-size: clamp(72px, 16vw, 210px);
  line-height: 0.88;
  letter-spacing: -0.02em;
  color: #f4f0e8;
  text-transform: uppercase;
  will-change: transform;
}
.plx-07__display.is-outline {
  color: transparent;
  -webkit-text-stroke: 2px rgba(244,240,232,0.25);
}
.plx-07__display.is-accent {
  color: #dca03c;
}

/* S1 progress-driven: bg scrolls while text moves subtly in opposite dir */
/* S1 label */
.plx-07__s1-label {
  position: absolute;
  bottom: 40px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 9px;
  font-family: 'DM Sans', sans-serif;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  opacity: 0.3;
  white-space: nowrap;
}

/* ═══ SCENE 2: Quote — text and bg move at different rates ═══ */
.plx-07__s2 {
  position: relative;
  /* See plx-07__s1 comment above for rationale. */
  height: 300vh;
  background: #0a0e12;
}
.plx-07__s2-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
}

.plx-07__s2-bg {
  position: absolute;
  inset: -30%;
  will-change: transform;
  background:
    repeating-linear-gradient(
      25deg,
      rgba(100,160,220,0.0) 0px,
      rgba(100,160,220,0.0) 50px,
      rgba(100,160,220,0.05) 50px,
      rgba(100,160,220,0.05) 52px
    ),
    radial-gradient(ellipse at 70% 60%, rgba(60,100,180,0.3) 0%, transparent 55%),
    radial-gradient(ellipse at 20% 30%, rgba(180,60,120,0.2) 0%, transparent 50%),
    linear-gradient(160deg, #060810 0%, #0a0e14 100%);
}

.plx-07__quote-wrap {
  position: relative;
  z-index: 5;
  padding: 0 clamp(40px, 10vw, 140px);
  will-change: transform;
  max-width: 1100px;
}

.plx-07__qmark {
  display: block;
  font-family: 'Lora', serif;
  font-size: clamp(120px, 20vw, 220px);
  line-height: 0.7;
  color: rgba(100,160,220,0.2);
  margin-bottom: -24px;
}

.plx-07__qtext {
  font-family: 'Lora', serif;
  font-style: italic;
  font-size: clamp(24px, 4.5vw, 58px);
  line-height: 1.35;
  font-weight: 400;
  color: #f4f0e8;
}
.plx-07__qtext em {
  font-style: normal;
  color: #7eb8e0;
}

.plx-07__qattr {
  display: block;
  margin-top: 32px;
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  opacity: 0.35;
  font-style: normal;
  font-family: 'DM Sans', sans-serif;
}

/* ═══ SCENE 3: Giant word — top half normal, bottom half outline, bg scrolls ═══ */
.plx-07__s3 {
  position: relative;
  /* See plx-07__s1 comment above for rationale. */
  height: 300vh;
  background: #100c08;
}
.plx-07__s3-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.plx-07__s3-bg {
  position: absolute;
  inset: -30%;
  will-change: transform;
  background:
    repeating-linear-gradient(
      0deg,
      rgba(220,140,60,0.0) 0px,
      rgba(220,140,60,0.0) 20px,
      rgba(220,140,60,0.05) 20px,
      rgba(220,140,60,0.05) 21px
    ),
    radial-gradient(ellipse at 60% 45%, rgba(200,100,20,0.3) 0%, transparent 55%),
    linear-gradient(160deg, #100c06 0%, #180e04 100%);
}

/* Giant split word */
.plx-07__word-wrap {
  position: relative;
  z-index: 5;
  padding-right: clamp(30px, 8vw, 120px);
  will-change: transform;
  line-height: 0.88;
}

.plx-07__word-solid {
  display: block;
  font-family: 'Anton', sans-serif;
  font-size: clamp(90px, 20vw, 270px);
  letter-spacing: -0.04em;
  color: #f4f0e8;
  /* clip to top 50% */
  clip-path: inset(0 0 50% 0);
  line-height: 0.88;
}
.plx-07__word-outline {
  display: block;
  font-family: 'Anton', sans-serif;
  font-size: clamp(90px, 20vw, 270px);
  letter-spacing: -0.04em;
  color: transparent;
  -webkit-text-stroke: 2px rgba(244,240,232,0.4);
  /* offset up to align perfectly */
  margin-top: calc(clamp(90px, 20vw, 270px) * -0.88);
  clip-path: inset(50% 0 0 0);
  line-height: 0.88;
}

/* Caption text left side */
.plx-07__s3-caption {
  position: absolute;
  left: clamp(30px, 6vw, 80px);
  bottom: 60px;
  z-index: 6;
  max-width: 260px;
}
.plx-07__s3-caption h3 {
  font-family: 'Lora', serif;
  font-style: italic;
  font-size: clamp(18px, 2.5vw, 26px);
  font-weight: 400;
  line-height: 1.4;
  margin-bottom: 12px;
}
.plx-07__s3-caption p {
  font-size: 13px;
  font-weight: 300;
  line-height: 1.8;
  opacity: 0.45;
}

/* ═══ Footer ═══ */
.plx-07__footer {
  background: #f4f0e8;
  color: #0c0c0a;
  padding: 100px 60px;
  text-align: center;
}
.plx-07__footer h2 {
  font-family: 'Anton', sans-serif;
  font-size: clamp(32px, 6vw, 80px);
  letter-spacing: -0.03em;
  text-transform: uppercase;
  margin-bottom: 20px;
}
.plx-07__footer h2 em {
  font-family: 'Lora', serif;
  font-style: italic;
  font-weight: 400;
  color: #b87020;
}
.plx-07__footer p {
  font-size: 15px;
  font-weight: 300;
  line-height: 1.8;
  opacity: 0.55;
  max-width: 480px;
  margin: 0 auto;
}

@media (max-width: 600px) {
  .plx-07__s3-caption { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .plx-07__s1-bg, .plx-07__s2-bg, .plx-07__s2-bg, .plx-07__s3-bg,
  .plx-07__quote-wrap, .plx-07__word-wrap, .plx-07__display { transform: none !important; }
}
(() => {
  const s1 = document.querySelector('.plx-07__s1');
  const s2 = document.querySelector('.plx-07__s2');
  const s3 = document.querySelector('.plx-07__s3');

  const s1bg = document.getElementById('plx07-s1-bg');
  const line1 = document.getElementById('plx07-line1');
  const line2 = document.getElementById('plx07-line2');
  const line3 = document.getElementById('plx07-line3');

  const s2bg = document.getElementById('plx07-s2-bg');
  const quote = document.getElementById('plx07-quote');

  const s3bg = document.getElementById('plx07-s3-bg');
  const word = document.getElementById('plx07-word');

  let ticking = false;

  /* progress(el): sticky-pin progress, 0 at pin engage → 1 at pin
     release. Uses (offsetHeight - innerHeight) as the divisor which
     is the pin runway in scroll px. With scene at 300vh + sticky
     inner at 100vh, the runway is 200vh of scroll. */
  function progress(el) {
    if (!el) return 0;
    const r = el.getBoundingClientRect();
    const h = el.offsetHeight - window.innerHeight;
    return h > 0 ? Math.max(0, Math.min(1, -r.top / h)) : 0;
  }

  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      // Scene 1: bg scrolls up fast, text drifts slightly
      const p1 = progress(s1);
      if (s1bg) s1bg.style.transform = `translateY(${p1 * -120}px)`;
      if (line1) line1.style.transform = `translateY(${p1 * 15}px)`;
      if (line2) line2.style.transform = `translateY(${p1 * 0}px)`;
      if (line3) line3.style.transform = `translateY(${p1 * -15}px)`;

      // Scene 2: bg scrolls fast, quote drifts slowly upward
      const p2 = progress(s2);
      if (s2bg) s2bg.style.transform = `translateY(${p2 * -140}px)`;
      if (quote) quote.style.transform = `translateY(${p2 * -40}px)`;

      // Scene 3: bg scrolls, word barely moves (anchored feel)
      const p3 = progress(s3);
      if (s3bg) s3bg.style.transform = `translateY(${p3 * -120}px) skewY(${p3 * 1.5}deg)`;
      if (word) word.style.transform = `translateY(${p3 * -12}px)`;

      ticking = false;
    });
  }

  window.addEventListener('scroll', onScroll, { passive: true });
  onScroll();
})();

Search CodeFronts

Loading…