See the Pen Bind CSS keyframe animation to scroll by Chris Coyier (@chriscoyier) on CodePen.
See the Pen scroll timeline cube by lee (@leemeyer) on CodePen.
window.addEventListener('scroll', () => {
document.body.style.setProperty('--scroll',
window.pageYOffset / (document.body.offsetHeight - window.innerHeight));
}, false);
body, .progress, .cube {
/* Pause the animation */
animation-play-state: paused;
/* Bind the animation to scroll */
animation-delay: calc(var(--scroll) * -1s);
/* These last 2 properites clean up overshoot weirdness */
animation-iteration-count: 1;
animation-fill-mode: both;
}
body, .progress, .cube {
animation-timeline: scroll();
}
See the Pen scroll linked 3D demo by lee (@leemeyer) on CodePen.
.scene {
transform: rotateY(var(--my-y-angle)) rotateX(var(--my-x-angle));
}
body {
timeline-scope: --myScroller,--myScroller2;
}
.card:first-child {
scroll-timeline-axis: x;
scroll-timeline-name: --myScroller;
}
.card:nth-child(2) {
scroll-timeline-axis: y;
scroll-timeline-name: --myScroller2;
}
.scene {
animation: rotateHorizontal,rotateVertical;
animation-timeline: --myScroller,--myScroller2;
}
@keyframes rotateHorizontal {
to {
--my-y-angle: 360deg;
}
}
@keyframes rotateVertical {
to {
--my-x-angle: 360deg;
}
}
See the Pen scroll-linked X-wing by lee (@leemeyer) on CodePen.
See the Pen Web-slinger.css demo by lee (@leemeyer) on CodePen.
https://css-tricks.com/web-slinger-css-like-wow-js-but-with-css-y-scroll-animations/See the Pen collision detection using style queries plus keyframes by lee (@leemeyer) on CodePen.
body {
--int-ball-position-x: round(down, var(--ball-position-x));
--min-ball-position-y-and-top-of-paddle: min(
var(--ball-position-y) + var(--ball-height),
var(--ping-position)
);
--min-ball-position-y-and-bottom-of-paddle: min(
var(--ball-position-y),
var(--ping-position) + var(--paddle-height)
);
}
@container style(--min-ball-position-y-and-top-of-paddle: var(--ping-position))
and style(--min-ball-position-y-and-bottom-of-paddle: var(--ball-position-y))
and style(--int-ball-position-x: var(--ball-left-boundary)) {
.screen {
--lives-decrement: paused;
.field {
background: green;
}
}
}
@container style(--int-ball-position-x: var(--ball-left-boundary)) {
.screen {
--lives-decrement: running;
.field {
background: red;
}
}
}
body {
--int-ball-position-x: round(down, var(--ball-position-x));
--bottom-of-paddle-y: calc(var(--ping-position) + var(--paddle-height));
}
@container style(--ping-position < --ball-position-y)
and style(--ball-position-y < --bottom-of-paddle-y)
and style(--int-ball-position-x: var(--ball-left-boundary)) {
.screen {
--lives-decrement: paused;
.field {
background: green;
}
}
}
@container style(--int-ball-position-x: var(--ball-left-boundary)) {
.screen {
--lives-decrement: running;
.field {
background: red;
}
}
}
@container scroll-state(scrollable: top) {}
@container scroll-state(scrollable: right) {}
@container scroll-state(scrollable: bottom) {}
@container scroll-state(scrollable: left) {}
See the Pen Nonlinear Scrollytelling by lee (@leemeyer) on CodePen.
.idle {
animation: idleAnim 1s steps(6) infinite;
}
/*scroll direction detection using scroll timelines
https://www.bram.us/2023/10/23/css-scroll-detection*/
.sprite {
transform: rotateY(calc(1deg *
min(0, var(--scroll-direction) * 180)));
}
@container not style(--scroll-direction: 0) {
.sprite {
animation: runAnim 0.8s steps(8) infinite;
}
}
.sky, .buildings-back, .buildings-mid, .sky-vertical, .buildings-back-vertical, .buildings-mid-vertical {
position: fixed;
top: 0;
left: 0;
width: 800%;
height: max(100vh, 300px);
background-size: auto max(100vh, 300px);
background-repeat: repeat-x;
animation-timing-function: linear;
animation-timeline: scroll(x);
}
/*...repetitively assign the corresponding animations to each layer...*/
@keyframes move-sky {
from {
transform: translateX(0);
}
to {
transform: translateX(-2.5%);
}
}
@keyframes move-back {
from {
transform: translateX(0);
}
to {
transform: translateX(-6.25%);
}
}
@keyframes move-mid {
from {
transform: translateX(0);
}
to {
transform: translateX(-12.5%);
}
}
@container scroll-state((scrollable: left)) {
body {
overflow-y: hidden;
}
}
@container scroll-state((scrollable: bottom)) {
body {
width: 0;
}
}
:root {
animation: adjust-pos 1s linear, move-sprite-up 1s linear, move-player 1s linear;
animation-timeline: scroll(x), scroll(y), scroll(x);
container-type: scroll-state;
.ladder {
height: 300vh;
}
.spawn-point {
position: absolute;
left: 400vw;
scroll-initial-target: nearest;
}
}
@keyframes collect-saber {
from {
--player-has-saber: false;
}
to {
--player-has-saber: true;
}
}
body {
animation: .25s forwards var(--saber-collection-state, paused) collect-saber;
}
@container scroll-state(not (scrollable: top)) {
body {
--saber-collection-state: running;
}
}
@container style(--player-has-saber: true) {
.sprite {
background-image: url(/*combat spritesheet*/);
}
.lightsaber {
visibility: hidden;
}
}
body {
--min-of-player-and-enemy-x: min(var(--player-x-offset), var(--enemy-x-offset) - 10px);
--max-of-player-and-enemy-y: max(var(--player-y-offset, 5px));
--game-state: if(style(--min-of-player-and-enemy-x: calc(var(--enemy-x-offset) - 10px)) and style(--max-of-player-and-enemy-y: 5px): ending;
else: playing);
overflow: if(style(--game-state: ending): hidden;
else: scroll);
}
@container style(--player-has-saber: true) and style(--game-state: ending) {
.player-wrapper {
.sprite {
animation: attack 0.7s steps(4) forwards;
}
.speech-bubble {
animation: show-endgame-message 3s linear 1s forwards;
&::before {
content: 'Refresh the page to play again';
}
}
.evil-twin-wrapper {
.evil-twin {
evil-twin-die 0.8s steps(4) .7s forwards;
}
}
}
@container style(--player-has-saber: false) and style(--game-state: ending) {
.player-wrapper {
.sprite {
animation: player-die .8s steps(6) .7s forwards;
}
}
.evil-twin-wrapper {
.speech-bubble {
animation: show-endgame-message 3s linear 1s forwards;
display: block;
&::before {
content: 'Baha! Refresh the page to fight me again';
}
.evil-twin {
attack 0.8s steps(4) infinite;
}
}
}
}
@container scroll-state((scrollable: top) and ((scrollable: bottom))) {
.player-wrapper {
.sprite {
animation: climbAnim 1s steps(8);
animation-timeline: scroll(root y);
animation-iteration-count: 10;
}
}
}