Parallax-Effekt beim Scrollen erstellen

Ein Parallaxeneffekt beim Scrollen ist eine visuelle Technik, bei der sich verschiedene Inhaltsebenen mit unterschiedlicher Geschwindigkeit bewegen, wenn der Nutzer auf einer Seite nach unten scrollt. Dadurch entsteht eine Tiefenillusion, bei der sich Elemente im Vordergrund schneller zu bewegen scheinen als Elemente im Hintergrund. Das sorgt für ein ansprechendes und immersives Surferlebnis. Dieser Effekt lässt sich am besten mit scrollgesteuerten CSS-Animationen erzielen, mit denen Sie Animationen mit der Scrollposition eines Containers verknüpfen können.

Implementierung

So erstellen Sie einen einfachen Parallax-Effekt:

  1. Wrapper-Element erstellen: Mit diesem Element werden alle Ebenen des Parallax-Effekts gruppiert. Es ist nicht das scrollbare Element, daher sollte sein Überlauf abgeschnitten werden. Geben Sie außerdem einen height-Wert an, der der Höhe einer der Ebenen des Parallax-Effekts entspricht.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Ebenen deklarieren: Fügen Sie im Wrapper die einzelnen Ebenen hinzu, die sich mit unterschiedlichen Geschwindigkeiten bewegen sollen.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Verschiebungsanimation hinzufügen: Definieren Sie eine CSS-Animation, die die transform-Eigenschaft der Ebenen ändert. Für einen Parallaxeneffekt verwenden Sie normalerweise translateY, um die Ebenen vertikal zu verschieben.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. view-timeline einrichten: Wenn Sie die Animation mit der Scrollposition verknüpfen möchten, erstellen Sie eine view-timeline für das Wrapper-Element und wenden Sie sie dann auf die Ebenen an.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. Animationen versetzen: Damit sich die Ebenen mit unterschiedlichen Geschwindigkeiten bewegen, können Sie einen von zwei Hauptansätzen verwenden: Versatz in den Keyframes oder Versatz der animation-range.

    Bei beiden Ansätzen können hartcodierte Werte oder die sibling-index()/sibling-count()-Implementierung verwendet werden. Die hartcodierten Werte sind am einfachsten und auch nützlich, wenn nur eine begrenzte Anzahl von Ebenen vorhanden ist. Die sibling-index()/sibling-count()-Implementierung ist praktisch, wenn Sie viele Ebenen haben.

    • Keyframes versetzen:

      Mit hartcodierten Werten können Sie für jede Ebene eine benutzerdefinierte Eigenschaft definieren, um den Parallaxenversatz manuell zu steuern.

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      Mit sibling-index() können Sie die Funktion sibling-index() verwenden, um den Index eines untergeordneten Elements unter seinen gleichgeordneten Elementen zurückzugeben und den gestaffelten Effekt automatisch zu berechnen.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • animation-range zeitlich versetzt ausführen:

      Mit hartcodierten Werten können Sie die Grenzen des animation-range auf jeder Ebene einzeln definieren.

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      Mit sibling-index() und sibling-count() können Sie den Bereich mathematisch anhand der Gesamtzahl der Ebenen (sibling-count()) berechnen.

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

Beispielcode

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

Alternativ können Sie die Property animation-range verwenden, um einen ähnlichen Effekt zu erzielen:

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

Best Practices

Wenn Sie scrollbasierte Animationen verwenden, sollten Sie einige Best Practices beachten, um eine reibungslose und barrierefreie Nutzung zu ermöglichen:

  • DO include feature detection: Not all browsers support scroll-driven animations. Verwenden Sie @supports ((animation-timeline: view()) and (animation-range: entry)), um die Unterstützung zu prüfen, und stellen Sie einen Fallback für Browser bereit, die die Funktion nicht unterstützen.
    • Die (animation-range: entry)-Prüfung MUSS hier enthalten sein, um Browser mit nur teilweiser Unterstützung herauszufiltern.
    • Verwenden Sie NICHT das Paket scroll-timeline-polyfill für die Fallback-Strategie, da es nicht vollständig ist und viele bekannte Probleme aufweist.
    • Wenn die Animation nur dekorativ ist, entscheide dich für die progressive Verbesserung und stelle KEIN Fallback bereit.
  • DO respect user preferences: Some users prefer to have less motion on the web. Verwenden Sie die Medienabfrage prefers-reduced-motion, um Ihre Animationen für diese Nutzer zu deaktivieren oder zu reduzieren.
  • DO try to animate only performant CSS properties: For the smoothest animations, stick to animating properties that can be handled by the browser's compositor thread, such as transform and opacity. Das Animieren anderer Attribute wie width oder height kann zu Leistungsproblemen führen.
  • RICHTIG: Verwenden Sie die richtige Deklarationsreihenfolge. Wenn Sie die Kurzschreibweise animation verwenden, deklarieren Sie animation-timeline und animation-range danach, um zu verhindern, dass die Kurzschreibweise die Zeitachse zurücksetzt.

So legen Sie die animation-range fest:

  • RICHTIG: Alle Ebenen sollten denselben Start-Offset haben, z. B. entry 25%.
  • DO give all layers a different end offset that uses sibling-count() and sibling-index() to distribute the offsets, e.g. exit calc(100% / sibling-count() * sibling-index()).

Browserunterstützung und Fallback-Strategien

Daher ist in der Regel eine Fallback-Strategie erforderlich.

Für Browser, die keine scrollgesteuerten Animationen unterstützen, können Sie einen Fallback verwenden, um die visuellen Effekte nachzubilden. Die Fallbacks werden in der Regel entweder mit einem Scroll-Listener (für ScrollTimeline-Effekte) oder der IntersectionObserver API (für ViewTimeline-Effekte) erstellt.

Verwenden Sie in Browsern mit integrierter Unterstützung für scrollgesteuerte Animationen IMMER die native CSS-Implementierung, da diese leistungsfähiger ist.

Beachten Sie, dass nicht jeder Effekt mit dem Fallback-Ansatz nachgebildet werden kann.

Speziell für diesen Anwendungsfall wird im folgenden Script der Fallback für Browser angewendet, die keine scrollgesteuerten Animationen unterstützen. Es wird ein IntersectionObserver verwendet, um die Sichtbarkeit des .wrapper-Elements zu verfolgen, und die transform-Eigenschaft der Ebenen wird basierend auf der Scrollposition aktualisiert.

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}