fix(parallax): animate background-position-y directly so SSR parallax works pre-WASM

This commit is contained in:
daniel-c-harvey
2026-06-11 14:45:30 -04:00
parent 9d7f2ff003
commit ae531116b7
5 changed files with 130 additions and 96 deletions
@@ -87,11 +87,11 @@ public abstract class ParallaxImageBase : ComponentBase, IAsyncDisposable
private string? _handle;
// --parallax-from/--parallax-to are inherited custom properties read by the
// CSS @keyframes parallax-pan (scroll-driven animation on .parallax-window).
// They encode ParallaxSpeed and InvertDirection. --parallax-pos itself is
// never set inline — an inline value would beat the CSS animation in the
// cascade and defeat the pre-WASM scroll-driven parallax. JS sets it inline
// only after WASM boots, transparently taking over (inline > animation).
// CSS @keyframes parallax-pan, which animates background-position-y on each
// .layer (scroll-driven, pre-WASM). They encode ParallaxSpeed and
// InvertDirection. After WASM boots, parallax.js sets data-parallax-active to
// cancel that animation and drives background-position-y on the layers
// directly — a clean handoff with no competing writers.
private string ParallaxVars()
{
var end = (int)Math.Round(ParallaxSpeed * 100);
@@ -1,87 +0,0 @@
@property --parallax-pos {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
.parallax-window {
position: relative;
overflow: hidden;
height: var(--window-height, 300px);
width: 100%;
}
.parallax-window.full-width {
width: 100vw;
position: relative;
left: 50%;
right: 50%;
margin-left: -50vw;
margin-right: -50vw;
}
.layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-repeat: no-repeat;
background-position: 50% var(--parallax-pos, 50%);
}
.layer-1 {
opacity: 1;
}
.layer-2 {
opacity: 0;
transition: opacity 700ms ease;
}
.parallax-window:hover .layer-2 {
opacity: 1;
}
/*
* Cascade interaction for --parallax-pos:
*
* Before WASM (SSR / hydration):
* @property initial-value: 0%
* → CSS animation (view timeline) overrides → correct position from scroll
*
* After WASM (JS running):
* JS element.style.setProperty('--parallax-pos', ...) [inline style]
* → inline style beats animation → JS takes over seamlessly
*
* prefers-reduced-motion:
* animation: none → @property initial-value 0% used → static image
* JS also skips scroll listener
*/
@supports (animation-timeline: view()) {
@keyframes parallax-pan {
from { --parallax-pos: var(--parallax-from, 0%); }
to { --parallax-pos: var(--parallax-to, 0%); }
}
.parallax-window {
animation: parallax-pan linear both;
animation-timeline: view();
}
@media (prefers-reduced-motion: reduce) {
.parallax-window {
animation: none;
}
}
}
@media (prefers-reduced-motion: reduce) {
.parallax-window {
--parallax-pos: 0%;
}
.layer-2 {
transition-duration: 0ms;
}
}