1
0

feat: implement prefers reduced motion for anims

This commit is contained in:
2026-03-09 11:45:34 +01:00
parent 9bfae3bcee
commit c1e9c5b127
10 changed files with 473 additions and 456 deletions
+15 -10
View File
@@ -148,15 +148,20 @@ function getCurrentYear() {
const footer = document.querySelector("footer"); const footer = document.querySelector("footer");
if (footer) { if (footer) {
gsap.from(footer, { const mm = gsap.matchMedia();
scrollTrigger: {
trigger: footer, mm.add("(prefers-reduced-motion: no-preference)", () => {
start: "top bottom", gsap.from(footer, {
}, scrollTrigger: {
duration: 2, trigger: footer,
opacity: 0, start: "top bottom",
yPercent: 10, },
ease: "expo.out", duration: 2,
opacity: 0,
yPercent: 10,
// yPercent: 10,
ease: "expo.out",
});
}); });
} }
</script> </script>
+11 -7
View File
@@ -82,14 +82,18 @@ import SkipNavLink from "./SkipNavLink.astro";
const header = document.querySelector("header"); const header = document.querySelector("header");
if (header) { if (header) {
const tl = gsap.timeline(); const mm = gsap.matchMedia();
tl.from(header.children, { mm.add("(prefers-reduced-motion: no-preference)", () => {
duration: 1, const tl = gsap.timeline();
yPercent: -120,
opacity: 0, tl.from(header.children, {
stagger: 0.2, duration: 1,
ease: "expo.out", yPercent: -120,
opacity: 0,
stagger: 0.2,
ease: "expo.out",
});
}); });
} }
</script> </script>
+80 -76
View File
@@ -122,87 +122,91 @@ import { Picture } from "astro:assets";
const aboutSection = document.querySelector(".about"); const aboutSection = document.querySelector(".about");
if (aboutSection) { if (aboutSection) {
const tl = gsap.timeline({ const mm = gsap.matchMedia();
scrollTrigger: {
trigger: aboutSection,
start: "top 70%",
},
});
tl.addLabel("heading", 0); mm.add("(prefers-reduced-motion: no-preference)", () => {
tl.addLabel("heading-text", 0.6); const tl = gsap.timeline({
tl.addLabel("image", 0.8); scrollTrigger: {
tl.addLabel("image-text", 1.1); trigger: aboutSection,
start: "top 70%",
},
});
new SplitText(aboutSection.querySelector("h2"), { tl.addLabel("heading", 0);
type: "words, chars", tl.addLabel("heading-text", 0.6);
autoSplit: true, tl.addLabel("image", 0.8);
mask: "chars", tl.addLabel("image-text", 1.1);
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(aboutSection.querySelectorAll(".heading p"), { new SplitText(aboutSection.querySelector("h2"), {
type: "lines, words", type: "words, chars",
autoSplit: true, autoSplit: true,
mask: "lines", mask: "chars",
linesClass: "line", charsClass: "char",
onSplit: (self) => { onSplit: (self) => {
tl.from( tl.from(
self.lines, self.chars,
{ {
duration: 0.9, duration: 0.9,
yPercent: 105, yPercent: -120,
stagger: 0.06, scale: 1.2,
ease: "expo.out", stagger: 0.015,
onComplete: () => self.revert(), ease: "expo.out",
}, onComplete: () => self.revert(),
"heading-text", },
); "heading",
}, );
}); },
});
tl.from( new SplitText(aboutSection.querySelectorAll(".heading p"), {
aboutSection.querySelector(".background"), type: "lines, words",
{ autoSplit: true,
duration: 2, mask: "lines",
opacity: 0, linesClass: "line",
ease: "expo.out", onSplit: (self) => {
}, tl.from(
"image", self.lines,
); {
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading-text",
);
},
});
new SplitText(aboutSection.querySelectorAll(".image > p"), { tl.from(
type: "lines, words", aboutSection.querySelector(".background"),
autoSplit: true, {
mask: "lines", duration: 2,
linesClass: "line", opacity: 0,
onSplit: (self) => { ease: "expo.out",
tl.from( },
self.lines, "image",
{ );
duration: 0.9,
yPercent: 105, new SplitText(aboutSection.querySelectorAll(".image > p"), {
stagger: 0.06, type: "lines, words",
ease: "expo.out", autoSplit: true,
onComplete: () => self.revert(), mask: "lines",
}, linesClass: "line",
"image-text", onSplit: (self) => {
); tl.from(
}, self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"image-text",
);
},
});
}); });
} }
</script> </script>
+88 -84
View File
@@ -104,100 +104,104 @@ import ArrowUpRight from "../icons/ArrowUpRight.astro";
const contactSection = document.querySelector(".contact"); const contactSection = document.querySelector(".contact");
if (contactSection) { if (contactSection) {
const tl = gsap.timeline({ const mm = gsap.matchMedia();
scrollTrigger: {
trigger: contactSection,
start: "top 60%",
},
});
tl.addLabel("image", 0.2); mm.add("(prefers-reduced-motion: no-preference)", () => {
tl.addLabel("heading", 0.5); const tl = gsap.timeline({
tl.addLabel("text", 0.6); scrollTrigger: {
tl.addLabel("arrow", 0.6); trigger: contactSection,
start: "top 60%",
},
});
new SplitText(contactSection.querySelector("h2"), { tl.addLabel("image", 0.2);
type: "words, chars", tl.addLabel("heading", 0.5);
autoSplit: true, tl.addLabel("text", 0.6);
mask: "chars", tl.addLabel("arrow", 0.6);
charsClass: "char",
onSplit: (self) => { new SplitText(contactSection.querySelector("h2"), {
tl.from( type: "words, chars",
self.chars, autoSplit: true,
{ mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(contactSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
contactSection.querySelector(".image"),
{
duration: 2,
opacity: 0,
xPercent: -10,
ease: "expo.out",
},
"image",
);
new SplitText(contactSection.querySelector("a"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
scrollTrigger: {
trigger: contactSection,
start: "top 50%",
},
duration: 0.9, duration: 0.9,
yPercent: -120, yPercent: -120,
scale: 1.2, scale: 1.2,
stagger: 0.015, stagger: 0.015,
ease: "expo.out", ease: "expo.out",
onComplete: () => self.revert(), onComplete: () => self.revert(),
}, });
"heading", },
); });
},
});
new SplitText(contactSection.querySelector("p"), { tl.from(
type: "lines, words", contactSection.querySelector("a > svg"),
autoSplit: true, {
mask: "lines", duration: 1,
linesClass: "line", opacity: 0,
onSplit: (self) => { yPercent: -10,
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
contactSection.querySelector(".image"),
{
duration: 2,
opacity: 0,
xPercent: -10,
ease: "expo.out",
},
"image",
);
new SplitText(contactSection.querySelector("a"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
scrollTrigger: {
trigger: contactSection,
start: "top 50%",
},
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out", ease: "expo.out",
onComplete: () => self.revert(), },
}); "arrow",
}, );
}); });
tl.from(
contactSection.querySelector("a > svg"),
{
duration: 1,
opacity: 0,
yPercent: -10,
ease: "expo.out",
},
"arrow",
);
} }
</script> </script>
+69 -65
View File
@@ -125,74 +125,78 @@ import ArrowDown from "../icons/ArrowDown.astro";
const heroSection = document.querySelector(".hero"); const heroSection = document.querySelector(".hero");
if (heroSection) { if (heroSection) {
const tl = gsap.timeline().pause(); const mm = gsap.matchMedia();
tl.addLabel("heading", 0); mm.add("(prefers-reduced-motion: no-preference)", () => {
tl.addLabel("image", 0.2); const tl = gsap.timeline().pause();
tl.addLabel("text", 0.8);
tl.addLabel("arrow", 1.5);
tl.from( tl.addLabel("heading", 0);
heroSection.querySelector(".image"), tl.addLabel("image", 0.2);
{ tl.addLabel("text", 0.8);
duration: 2, tl.addLabel("arrow", 1.5);
opacity: 0,
xPercent: 10,
ease: "expo.out",
},
"image",
);
new SplitText(heroSection.querySelector("h1"), { tl.from(
type: "words, chars", heroSection.querySelector(".image"),
autoSplit: true, {
mask: "chars", duration: 2,
charsClass: "char", opacity: 0,
onSplit: (self) => { xPercent: 10,
tl.from( ease: "expo.out",
self.chars, },
{ "image",
duration: 1, );
yPercent: -120,
scale: 1.2, new SplitText(heroSection.querySelector("h1"), {
stagger: 0.015, type: "words, chars",
ease: "expo.out", autoSplit: true,
onComplete: () => self.revert(), mask: "chars",
}, charsClass: "char",
"heading", onSplit: (self) => {
); tl.from(
}, self.chars,
{
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(heroSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
).play();
},
});
tl.from(
heroSection.querySelector(".down"),
{
duration: 1,
opacity: 0,
translateY: "-100%",
ease: "bounce.out",
},
"arrow",
);
}); });
new SplitText(heroSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
).play();
},
});
tl.from(
heroSection.querySelector(".down"),
{
duration: 1,
opacity: 0,
translateY: "-100%",
ease: "bounce.out",
},
"arrow",
);
} }
</script> </script>
+66 -62
View File
@@ -110,68 +110,72 @@ const services: Array<ComponentProps<typeof ServicesCard>> = [
const servicesSection = document.querySelector(".services"); const servicesSection = document.querySelector(".services");
if (servicesSection) { if (servicesSection) {
const tl = gsap.timeline({ const mm = gsap.matchMedia();
scrollTrigger: {
trigger: servicesSection, mm.add("(prefers-reduced-motion: no-preference)", () => {
start: "top 60%", const tl = gsap.timeline({
}, scrollTrigger: {
trigger: servicesSection,
start: "top 60%",
},
});
tl.addLabel("heading", 0);
tl.addLabel("text", 0.3);
tl.addLabel("cards", 0.5);
new SplitText(servicesSection.querySelector("h2"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(servicesSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 1.5,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
servicesSection.querySelectorAll(".cards > *"),
{
duration: 1,
opacity: 0,
yPercent: 5,
stagger: 0.15,
ease: "expo.out",
},
"cards",
);
}); });
tl.addLabel("heading", 0);
tl.addLabel("text", 0.3);
tl.addLabel("cards", 0.5);
new SplitText(servicesSection.querySelector("h2"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(servicesSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 1.5,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
servicesSection.querySelectorAll(".cards > *"),
{
duration: 1,
opacity: 0,
yPercent: 5,
stagger: 0.15,
ease: "expo.out",
},
"cards",
);
} }
</script> </script>
+90 -86
View File
@@ -113,101 +113,105 @@ function formatIndex(index: number) {
const valuesSection = document.querySelector(".values"); const valuesSection = document.querySelector(".values");
if (valuesSection) { if (valuesSection) {
const heading = valuesSection.querySelector("h2"); const mm = gsap.matchMedia();
new SplitText(heading, { mm.add("(prefers-reduced-motion: no-preference)", () => {
type: "words, chars", const heading = valuesSection.querySelector("h2");
autoSplit: true,
mask: "chars", new SplitText(heading, {
charsClass: "char", type: "words, chars",
onSplit: (self) => { autoSplit: true,
gsap.from(self.chars, { mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
scrollTrigger: {
trigger: heading,
start: "top 70%",
},
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
const items = valuesSection.querySelectorAll("li");
items.forEach((item) => {
const itemTl = gsap.timeline({
scrollTrigger: { scrollTrigger: {
trigger: heading, trigger: item,
start: "top 70%", start: "top 70%",
}, },
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
}); });
},
});
const items = valuesSection.querySelectorAll("li"); itemTl.addLabel("number", 0);
itemTl.addLabel("heading", 0.2);
itemTl.addLabel("text", 0.5);
items.forEach((item) => { new SplitText(item.querySelector("span"), {
const itemTl = gsap.timeline({ type: "words, chars",
scrollTrigger: { autoSplit: true,
trigger: item, mask: "chars",
start: "top 70%", charsClass: "char",
}, onSplit: (self) => {
}); itemTl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
stagger: 0.25,
ease: "expo.out",
onComplete: () => self.revert(),
},
"number",
);
},
});
itemTl.addLabel("number", 0); new SplitText(item.querySelector("h3"), {
itemTl.addLabel("heading", 0.2); type: "words, chars",
itemTl.addLabel("text", 0.5); autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
itemTl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(item.querySelector("span"), { new SplitText(item.querySelector("p"), {
type: "words, chars", type: "lines, words",
autoSplit: true, autoSplit: true,
mask: "chars", mask: "lines",
charsClass: "char", linesClass: "line",
onSplit: (self) => { onSplit: (self) => {
itemTl.from( itemTl.from(
self.chars, self.lines,
{ {
duration: 0.9, duration: 0.9,
yPercent: -120, yPercent: 105,
stagger: 0.25, stagger: 0.06,
ease: "expo.out", ease: "expo.out",
onComplete: () => self.revert(), onComplete: () => self.revert(),
}, },
"number", "text",
); );
}, },
}); });
new SplitText(item.querySelector("h3"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
itemTl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(item.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
itemTl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
}); });
}); });
} }
+27 -23
View File
@@ -63,30 +63,34 @@ import BaseLayout from "../layouts/BaseLayout.astro";
const page = document.querySelector(".imprint"); const page = document.querySelector(".imprint");
if (page) { if (page) {
new SplitText(page.querySelector("h1"), { const mm = gsap.matchMedia();
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
gsap.from(page.querySelectorAll(":scope > div"), { mm.add("(prefers-reduced-motion: no-preference)", () => {
duration: 1, new SplitText(page.querySelector("h1"), {
delay: 0.3, type: "words, chars",
opacity: 0, autoSplit: true,
yPercent: -20, mask: "chars",
ease: "expo.out", charsClass: "char",
stagger: 0.1, onSplit: (self) => {
gsap.from(self.chars, {
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
gsap.from(page.querySelectorAll(":scope > div"), {
duration: 1,
delay: 0.3,
opacity: 0,
yPercent: -20,
ease: "expo.out",
stagger: 0.1,
});
}); });
} }
</script> </script>
-20
View File
@@ -1,20 +0,0 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
const allPosts = Object.values(
import.meta.glob("./posts/*.md", { eager: true }),
);
const pageTitle = "My Astro Learning Blog";
---
<BaseLayout pageTitle={pageTitle}>
<p>This is where I will post about my journey learning Astro.</p>
<ul>
{
allPosts.map((post: any) => (
<li>
<a href={post.url}>{post.frontmatter.title}</a>
</li>
))
}
</ul>
</BaseLayout>
+27 -23
View File
@@ -134,30 +134,34 @@ import BaseLayout from "../layouts/BaseLayout.astro";
const page = document.querySelector(".privacy"); const page = document.querySelector(".privacy");
if (page) { if (page) {
new SplitText(page.querySelector("h1"), { const mm = gsap.matchMedia();
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
gsap.from(page.querySelectorAll(".content > *"), { mm.add("(prefers-reduced-motion: no-preference)", () => {
duration: 1, new SplitText(page.querySelector("h1"), {
delay: 0.3, type: "words, chars",
opacity: 0, autoSplit: true,
yPercent: -20, mask: "chars",
ease: "expo.out", charsClass: "char",
stagger: 0.1, onSplit: (self) => {
gsap.from(self.chars, {
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
gsap.from(page.querySelectorAll(".content > *"), {
duration: 1,
delay: 0.3,
opacity: 0,
yPercent: -20,
ease: "expo.out",
stagger: 0.1,
});
}); });
} }
</script> </script>