01 / 22
Nerdtalk 08.07.2026

Magische Effekte
– jetzt in CSS.

View Transitions Scroll-driven Animations + Ausblick: scroll-triggered

Drei neue native Browser-APIs, ein gemeinsames Versprechen: komplexe Animationseffekte nativ — ohne Framer Motion, GSAP oder umständliches JS.

Dieses Deck nutzt Übergänge per View-Transitions-API · Scroll-Demos laufen live · Pfeiltasten zum Blättern

Sektion I

View
Transitions.

Snapshot alt, DOM-Update, Snapshot neu — der Browser animiert den Diff. Erst Cross-Document (CSS only), dann Same-Document (JS/LiveView).

View Transitions

Snapshot alt → DOM-Update → Snapshot neu

Der Browser fotografiert die Seite vorher und nachher, animiert den Diff. Default: Crossfade.

View Transitions

Cross-Document — CSS only

Same-Origin-Navigation. Kein JS nötig. Eine Zeile CSS auf beiden Seiten — Default: Crossfade.


@view-transition { navigation: auto; }
View Transitions

Same-Document – JS API

Dieselbe API, verschiedene Effekte: Default-Crossfade — oder Masken-Reveal via clipPath auf dem ::view-transition-new(root)-Pseudo.

View Transitions

Shared-Element-Morph

Setz view-transition-name statisch aufs Element. Jede Layout-Änderung wird zur flüssigen Interpolation — Position, Größe, Border-Radius. Kein JS-Umschieben von Namen.

/* Eine Zeile am Container reicht */
.card { view-transition-name: card; }
// JS: ein Klick, ein Class-Toggle
card.onclick = () => document.startViewTransition(
  () => card.classList.toggle("open")
);
View Transitions

Und beim Karten-Grid?

Namen müssen pro Dokument eindeutig sein. Bei 20 Karten also nicht 20-mal derselbe Name. Zwei Wege:

/* 1) Eindeutiger Name je Karte (per data-attr in JS oder inline var) */
.card[data-id="42"]{ view-transition-name: card-42; }

/* 2) view-transition-class: eine Klasse deckt viele Snapshots ab */
.card       { view-transition-class: card; }
.card.active{ view-transition-name: card-active; }   /* aktiv hat einzigartigen Namen */

/* Regeln nun via Klasse ansprechbar */
::view-transition-group(.card){
  animation-timing-function: cubic-bezier(.7,0,.2,1);
}
Pattern: nur die angeklickte Karte bekommt den geteilten Namen vor dem Snapshot — nicht alle gleichzeitig.
View Transitions

Same-Document — Phoenix LiveView

Seit LiveView 1.1.18: onDocumentPatch im dom-Objekt. Der eigentliche Patch wird gewrappt — kein applyDiff-Monkeypatching mehr.

const liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks,
  dom: {
    onDocumentPatch(apply){                        // apply() = der eigentliche Patch
      if(!document.startViewTransition) return apply();
      document.startViewTransition(() => apply());
    }
  }
});
Wichtig: view-transition-name vor dem Patch setzen (im mounted()-Hook oder statisch im Markup), nicht nachträglich per Patch.
View Transitions

Selektiv animieren — nur wenn's zählt

Ohne Filter flackert jeder Presence-, Timer- oder Form-Update. Server entscheidet per Event, wann.

let animateNext = false;
window.addEventListener("phx:view-transition", () => { animateNext = true; });

dom: {
  onDocumentPatch(apply){
    if(animateNext && document.startViewTransition){
      animateNext = false;
      document.startViewTransition(() => apply());
    } else { apply(); }
  }
}
{:noreply, push_event(socket, "view-transition", %{})}   # nur wenn animieren
View Transitions

Was schiefgeht — und wie nicht

Cross-doc-Haken zur Abgrenzung: pageswap (alte Seite) / pagereveal (neue Seite).

View Transitions

Browser Support

Chrome/Edge
Safari
Firefox
Baseline
Same-Document
111+
18+
144+
seit Okt 2025
Cross-Document
126+
18.2+
im Roll-out
Sektion II

Scroll-driven
Animations.

Animation per Scroll-Position steuern. animation-timeline: scroll() oder view() koppelt @keyframes an Container-Scroll bzw. Element-Sichtbarkeit — CSS statt JS Scroll-Listener.

Scroll-driven Animations

animation-timeline statt Zeit

Alles was du über @keyframes, animation-timing-function und fill-mode weißt, gilt weiter. Nur die Uhr ändert sich: statt Sekunden — Scroll-Position.

@keyframes reveal{ from{opacity:0} to{opacity:1} }

.item{
  animation: reveal 1ms linear both;      /* 1ms wegen Firefox-Quirk */
  animation-timeline: view();
  animation-range: entry 10% cover 40%;
}
Scroll-driven Animations

Zwei Timeline-Typen: scroll() · view()

.progress { animation-timeline: scroll(root); }
.reveal   { animation-timeline: view(); }
.local    { animation-timeline: scroll(nearest block); }
Scroll-driven Animations

animation-range — wann startet, wann endet's

animation-range: entry 10% cover 40%;
/*                 ↑ start           ↑ end
   erst wenn 10% des Elements sichtbar,
   fertig wenn 40% des Weges durch den Viewport zurückgelegt sind */
Scroll-driven Animations

Benannte Timelines — timeline-scope

Anonyme Timelines sind das häufigste Muster. Benannte trennen Scope: Kind-Element hört auf Timeline des Eltern-Containers.

.gallery{
  scroll-timeline-name: --gallery;
  scroll-timeline-axis: x;
  overflow-x: auto;
}
.gallery-progress{
  animation: fill 1ms linear both;
  animation-timeline: --gallery;
}
/* außerhalb: Scope hochziehen */
body{ timeline-scope: --gallery; }
Scroll-driven Animations

Vier Muster — live

Scroll-driven Animations

Achtung: view() sieht nur die Originalposition ohne Transform

Ein transform: translate() verschiebt nur die visuelle Darstellung — nicht die Layout-Box. Die Timeline (und damit Start/Ende der animation-range) bezieht sich auf die ursprüngliche, untransformierte Position.

Konsequenz: Wenn der Auslösepunkt zählt, positioniere per Layout — nicht per transform. Und ja, das ist genau der Grund, warum eine Reveal-Animation komisch losfeuert, wenn du das Element mit translate nach unten schiebst.
/* ❌ Range startet wo das Element STAND, nicht wo es sichtbar ist */
.item{
  transform: translateY(200px);         /* verschiebt die Pixel … */
  animation-timeline: view();           /* … aber nicht die Layout-Box */
}
Scroll-driven Animations

Progressive Enhancement · Firefox-1ms-Quirk

@supports (animation-timeline: view()){
  .item{
    animation: reveal 1ms linear both;   /* nicht 0ms — Firefox verlangt einen Wert */
    animation-timeline: view();
    animation-range: entry 10% cover 40%;
  }
}
@supports not (animation-timeline: view()){
  .item{ opacity: 1 }                   /* Fallback: einfach da */
}
Scroll-driven Animations

Browser Support

Chrome/Edge
Safari
Firefox
caniuse
scroll()/view()
115+
aktuell
Flag
~85%

Firefox: layout.css.scroll-driven-animations.enabled, in Nightly aktiv. Für Produktion: @supports-Fallback ist Pflicht — kein Effekt darf trotzdem eine nutzbare Seite ergeben.

Sektion III · Ausblick

Scroll-triggered
Animations.

Statt scrubben: triggern. Eine zeitbasierte Animation startet, sobald ein Element in den Viewport eintritt — Ersatz für IntersectionObserver. Aktuell nur Chrome 145+.

Ausblick — Scroll-triggered

Scrubben vs. Triggern

Scroll-driven koppelt die Animation an die Scroll-Position (Scrub). Scroll-triggered feuert eine ganz normale zeitbasierte Animation, sobald ein Range betreten wird.

Ausblick — Scroll-triggered

Syntax: timeline-trigger · animation-trigger

.subject{
  timeline-trigger: --t view() contain / cover;   /* Aktivierungs- / Aktiv-Range */
}
.box{
  animation: pop .35s ease both;
  animation-trigger: --t play-forwards play-backwards;
}
@keyframes pop{
  from{ opacity:0; transform: translateY(24px) scale(.7) }
  to  { opacity:1; transform: none }
}
Ausblick — Scroll-triggered

Browser Support

Chrome/Edge
Safari
Firefox
Status
animation-trigger
145+
experimentell
Feature-Detection zwingend — heute noch mit ehrlichem Fallback: CSS.supports("animation-trigger","--t play-forwards").
Weiterlesen

Quellen & Links

Scroll-driven Animations
Scroll-triggered