10 CSS Parallax Effects 01 / 10

CSS Parallax Hero Section

A luxury editorial hero built on five parallax layers — gradient bg, perspective grid, amber orbs, diagonal geometry SVG, and floating headline — each drifting at its own cadence.

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-01">

  <!-- HERO -->
  <section class="plx-01__hero">
    <div class="plx-01__bg-layer" data-parallax="0.5"></div>
    <div class="plx-01__grain"></div>

    <div class="plx-01__orb plx-01__orb--a" data-parallax="0.3"></div>
    <div class="plx-01__orb plx-01__orb--b" data-parallax="0.4"></div>

    <div class="plx-01__lines" data-parallax="0.25">
      <svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice">
        <line x1="0" y1="200" x2="1440" y2="700" stroke="#c8a95a" stroke-width="1"/>
        <line x1="0" y1="400" x2="1440" y2="900" stroke="#c8a95a" stroke-width="0.5"/>
        <line x1="200" y1="0" x2="800" y2="900" stroke="#c8a95a" stroke-width="0.5"/>
        <line x1="900" y1="0" x2="1440" y2="600" stroke="#c8a95a" stroke-width="1"/>
        <circle cx="720" cy="450" r="280" stroke="#c8a95a" stroke-width="0.5" fill="none"/>
        <circle cx="720" cy="450" r="180" stroke="#c8a95a" stroke-width="0.3" fill="none"/>
      </svg>
    </div>

    <div class="plx-01__content">
      <div class="plx-01__eyebrow">Est. 2024 · Design Studio</div>
      <h1 class="plx-01__headline" data-parallax="-0.15">
        WHERE<br>FORM<br><em>MEETS</em><br>FUNCTION
      </h1>
      <p class="plx-01__sub" data-parallax="-0.08">
        Crafting digital experiences that leave a lasting impression.
        We build at the intersection of art, technology, and human desire.
      </p>
      <div class="plx-01__cta">
        <button class="plx-01__btn plx-01__btn--primary">View Our Work</button>
        <button class="plx-01__btn plx-01__btn--secondary">Get in Touch</button>
      </div>
    </div>

    <div class="plx-01__scroll">
      <span>Scroll</span>
      <div class="plx-01__scroll-line"></div>
    </div>
  </section>

  <!-- SECTION -->
  <section class="plx-01__section">
    <div class="plx-01__section-img">
      <div class="plx-01__section-inner" data-parallax-section="0.3"></div>
    </div>
    <div class="plx-01__section-text">
      <h2>Precision in <span>Every</span> Pixel</h2>
      <div class="plx-01__rule"></div>
      <p>We approach design as a discipline of reduction — stripping away everything that isn't essential until only the pure signal remains.</p>
      <p>Every interaction is intentional. Every animation has a reason. Every color earns its place.</p>
      <div class="plx-01__stat-row">
        <div class="plx-01__stat"><strong>120+</strong><small>Projects</small></div>
        <div class="plx-01__stat"><strong>8yr</strong><small>Experience</small></div>
        <div class="plx-01__stat"><strong>14</strong><small>Awards</small></div>
      </div>
    </div>
  </section>

  <!-- BAND -->
  <div class="plx-01__band">
    <div class="plx-01__band-inner" data-parallax-band="0.4"></div>
    <div class="plx-01__marquee">
      DESIGN &nbsp;·&nbsp; DEVELOP &nbsp;·&nbsp; DELIVER &nbsp;·&nbsp; DESIGN &nbsp;·&nbsp; DEVELOP &nbsp;·&nbsp; DELIVER &nbsp;·&nbsp;
      DESIGN &nbsp;·&nbsp; DEVELOP &nbsp;·&nbsp; DELIVER &nbsp;·&nbsp; DESIGN &nbsp;·&nbsp; DEVELOP &nbsp;·&nbsp; DELIVER &nbsp;·&nbsp;
    </div>
    <div class="plx-01__band-text">We Build What Others Only Imagine</div>
  </div>

</div>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

.plx-01 {
  --ink: #0a0a0f;
  --cream: #f5f0e8;
  --gold: #c8a95a;
  --rust: #b84c2a;
  font-family: 'Inter', sans-serif;
  background: var(--ink);
  color: var(--cream);
  overflow-x: hidden;
}

/* ── Hero ── */
.plx-01__hero {
  position: relative;
  height: 100vh;
  min-height: 600px;
  overflow: hidden;
  display: flex;
  /* align-items: safe center — center the content when there's
     enough room, but fall back to flex-start when content would
     overflow. Without the safe keyword, flex centers content
     even when it overflows the container, which COMBINED with
     overflow:hidden above causes the top + bottom of the
     content to be clipped equally (the user reported "top and
     bottom of the view cut"). The safe keyword is the modern
     fix — Chrome 93+, Safari 16+, Firefox 63+. justify-content
     stays plain center since horizontal alignment never overflows
     in this layout. */
  align-items: safe center;
  justify-content: center;
  padding: 24px 0;
}

.plx-01__bg-layer {
  position: absolute;
  inset: -30%;
  background:
    radial-gradient(ellipse 80% 60% at 60% 40%, rgba(200,169,90,0.18) 0%, transparent 60%),
    radial-gradient(ellipse 40% 40% at 20% 70%, rgba(184,76,42,0.22) 0%, transparent 55%),
    linear-gradient(160deg, #0d0b18 0%, #1a0f0a 50%, #0a0d1a 100%);
  will-change: transform;
  transform: translateY(0px);
}

/* Grain texture overlay */
.plx-01__grain {
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
  background-size: 200px 200px;
  opacity: 0.5;
  pointer-events: none;
}

/* Geometric lines */
.plx-01__lines {
  position: absolute;
  inset: 0;
  will-change: transform;
  transform: translateY(0px);
}
.plx-01__lines svg {
  width: 100%; height: 100%;
  opacity: 0.12;
}

/* Floating orbs */
.plx-01__orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  will-change: transform;
  transform: translateY(0px);
}
.plx-01__orb--a {
  width: 420px; height: 420px;
  background: radial-gradient(circle, rgba(200,169,90,0.4) 0%, transparent 70%);
  top: -10%; right: -5%;
}
.plx-01__orb--b {
  width: 300px; height: 300px;
  background: radial-gradient(circle, rgba(184,76,42,0.35) 0%, transparent 70%);
  bottom: -5%; left: 5%;
}

/* Content — scrolls at normal speed */
.plx-01__content {
  position: relative;
  z-index: 10;
  text-align: center;
  padding: 0 24px;
  max-width: 900px;
}

.plx-01__eyebrow {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: var(--gold);
  border: 1px solid rgba(200,169,90,0.4);
  padding: 6px 18px;
  margin-bottom: 36px;
  border-radius: 2px;
}

.plx-01__headline {
  font-family: 'Bebas Neue', sans-serif;
  /* Sizing math (derived empirically, measured in browser):
     The headline has 4 forced <br> stacked lines + line-height 0.88,
     so its block height = font_size × 0.88 × 4 = font_size × 3.52.
     Other content + margins in the hero (eyebrow + sub + cta +
     padding + scroll-indicator clearance) measured to ~360px.
     For everything to fit in 100vh without clipping, headline block
     needs to be ≤ (100vh - 360px), so font_size ≤ (100vh - 360px) / 3.52.
     This calc-driven ceiling means the headline scales naturally
     with available viewport height: ~205px at desktop 1080px viewport
     (clamps to 180px ceiling from the design intent), ~83px in the
     playground iframe at 651px height (vs the previous 97px that
     overflowed the bottom CTAs), 56px floor on short mobile.
     14vw secondary constraint stops the headline being too wide on
     very wide-but-short viewports. Floor 56px handles 360px-wide
     phones. Ceiling 180px = 13rem from the demo desc. */
  font-size: clamp(56px, min(14vw, calc((100vh - 380px) / 3.52)), 180px);
  line-height: 0.88;
  letter-spacing: -0.01em;
  color: var(--cream);
  will-change: transform;
  transform: translateY(0px);
}

.plx-01__headline em {
  font-style: normal;
  display: block;
  background: linear-gradient(135deg, var(--gold) 0%, var(--rust) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.plx-01__sub {
  margin-top: 32px;
  font-size: 16px;
  font-weight: 300;
  line-height: 1.7;
  color: rgba(245,240,232,0.6);
  max-width: 500px;
  margin-left: auto;
  margin-right: auto;
  will-change: transform;
  transform: translateY(0px);
}

.plx-01__cta {
  margin-top: 48px;
  display: inline-flex;
  gap: 16px;
  flex-wrap: wrap;
  justify-content: center;
}

.plx-01__btn {
  padding: 14px 36px;
  font-size: 13px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  border: none;
  cursor: pointer;
  font-family: 'Inter', sans-serif;
  font-weight: 600;
  transition: transform 0.2s, box-shadow 0.2s;
}
.plx-01__btn--primary {
  background: linear-gradient(135deg, var(--gold), var(--rust));
  color: var(--ink);
}
.plx-01__btn--secondary {
  background: transparent;
  color: var(--cream);
  border: 1px solid rgba(245,240,232,0.3);
}
.plx-01__btn:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(200,169,90,0.25); }

/* Scroll indicator — original design: absolute-positioned at
   the hero's bottom-center, sits at the bottom edge of the
   100vh hero regardless of content height. Visible whenever
   content doesn't extend into the bottom 80px (which the new
   headline clamp prevents thanks to the 22vh component). */
.plx-01__scroll {
  position: absolute;
  bottom: 40px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  opacity: 0.5;
  z-index: 10;
  pointer-events: none;
}
.plx-01__scroll span {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.plx-01__scroll-line {
  width: 1px;
  height: 48px;
  background: linear-gradient(to bottom, var(--gold), transparent);
  animation: plx-01-scroll-pulse 2s ease-in-out infinite;
}
@keyframes plx-01-scroll-pulse {
  0%, 100% { opacity: 0.3; transform: scaleY(1); }
  50% { opacity: 1; transform: scaleY(0.6); }
}

/* ── Content sections below hero ── */
.plx-01__section {
  position: relative;
  padding: 120px 40px;
  max-width: 1100px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 80px;
  align-items: center;
}

.plx-01__section-img {
  position: relative;
  aspect-ratio: 4/5;
  overflow: hidden;
  border: 1px solid rgba(200,169,90,0.2);
}

.plx-01__section-inner {
  position: absolute;
  inset: -20%;
  background:
    linear-gradient(135deg, #1a120a 0%, #0f1520 100%);
  will-change: transform;
}

.plx-01__section-inner::after {
  content: '';
  position: absolute;
  inset: 20%;
  background: radial-gradient(circle at 40% 60%, rgba(200,169,90,0.3) 0%, rgba(184,76,42,0.2) 40%, transparent 70%);
  border: 1px solid rgba(200,169,90,0.15);
}

.plx-01__section-text h2 {
  font-family: 'Bebas Neue', sans-serif;
  font-size: clamp(42px, 6vw, 72px);
  line-height: 0.95;
  margin-bottom: 24px;
}

.plx-01__section-text h2 span {
  color: var(--gold);
}

.plx-01__section-text p {
  font-size: 15px;
  line-height: 1.75;
  color: rgba(245,240,232,0.6);
  margin-bottom: 16px;
}

.plx-01__rule {
  width: 48px;
  height: 2px;
  background: linear-gradient(90deg, var(--gold), transparent);
  margin: 32px 0;
}

.plx-01__stat-row {
  display: flex;
  gap: 40px;
  margin-top: 40px;
}

.plx-01__stat strong {
  display: block;
  font-family: 'Bebas Neue', sans-serif;
  font-size: 42px;
  color: var(--gold);
  line-height: 1;
}

.plx-01__stat small {
  font-size: 11px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(245,240,232,0.4);
}

/* Divider band */
.plx-01__band {
  background: linear-gradient(135deg, var(--gold) 0%, var(--rust) 100%);
  padding: 40px;
  text-align: center;
  overflow: hidden;
  position: relative;
}

.plx-01__band-inner {
  position: absolute;
  inset: -20%;
  will-change: transform;
}

.plx-01__marquee {
  font-family: 'Bebas Neue', sans-serif;
  font-size: 64px;
  color: rgba(10,10,15,0.12);
  white-space: nowrap;
  position: relative;
  z-index: 1;
  animation: plx-01-marquee 18s linear infinite;
}

@keyframes plx-01-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

.plx-01__band-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--ink);
  font-weight: 600;
}

@media (max-width: 768px) {
  .plx-01__section {
    grid-template-columns: 1fr;
    gap: 40px;
    padding: 80px 24px;
  }
  .plx-01__section-img { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .plx-01__bg-layer,
  .plx-01__lines,
  .plx-01__orb,
  .plx-01__headline,
  .plx-01__sub,
  .plx-01__section-inner { transform: none !important; }
  .plx-01__scroll-line { animation: none; }
  .plx-01__marquee { animation: none; }
}
(() => {
  const root = document.querySelector('.plx-01');
  if (!root) return;

  const hero = root.querySelector('.plx-01__hero');
  const heroH = hero.offsetHeight;

  // Elements with data-parallax on the hero
  const heroLayers = Array.from(root.querySelectorAll('[data-parallax]')).map(el => ({
    el,
    speed: parseFloat(el.dataset.parallax)
  }));

  // Section inner parallax
  const sectionInner = root.querySelector('[data-parallax-section]');
  const sectionSpeed = sectionInner ? parseFloat(sectionInner.dataset.parallaxSection) : 0.3;

  // Band inner parallax
  const bandInner = root.querySelector('[data-parallax-band]');
  const bandSpeed = bandInner ? parseFloat(bandInner.dataset.parallaxBand) : 0.4;

  let ticking = false;

  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const sy = window.scrollY;

      // Hero layers
      heroLayers.forEach(({ el, speed }) => {
        el.style.transform = `translateY(${sy * speed}px)`;
      });

      // Section inner — relative to its position
      if (sectionInner) {
        const rect = sectionInner.closest('.plx-01__section-img').getBoundingClientRect();
        const relY = rect.top + rect.height / 2;
        const offset = (relY - window.innerHeight / 2) * sectionSpeed;
        sectionInner.style.transform = `translateY(${-offset * 0.4}px)`;
      }

      // Band inner
      if (bandInner) {
        const rect = bandInner.closest('.plx-01__band').getBoundingClientRect();
        const relY = rect.top + rect.height / 2;
        const offset = (relY - window.innerHeight / 2) * bandSpeed;
        bandInner.style.transform = `translateX(${-offset * 0.2}px)`;
      }

      ticking = false;
    });
  }

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

Search CodeFronts

Loading…