88 lines
1.8 KiB
CSS
88 lines
1.8 KiB
CSS
@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 400ms 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;
|
|
}
|
|
}
|