From c1e9c5b1272a6947a629b7ca3e9f2ad2765301d1 Mon Sep 17 00:00:00 2001 From: Michael Rieger Date: Mon, 9 Mar 2026 11:45:34 +0100 Subject: [PATCH] feat: implement prefers reduced motion for anims --- src/components/Footer.astro | 25 +-- src/components/Header.astro | 18 +- src/components/landing/AboutSection.astro | 156 ++++++++-------- src/components/landing/ContactSection.astro | 172 +++++++++--------- src/components/landing/HeroSection.astro | 134 +++++++------- src/components/landing/ServicesSection.astro | 128 +++++++------- src/components/landing/ValuesSection.astro | 176 ++++++++++--------- src/pages/imprint.astro | 50 +++--- src/pages/old/blog.astro | 20 --- src/pages/privacy.astro | 50 +++--- 10 files changed, 473 insertions(+), 456 deletions(-) delete mode 100644 src/pages/old/blog.astro diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 653cfe9..dcabf63 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -148,15 +148,20 @@ function getCurrentYear() { const footer = document.querySelector("footer"); if (footer) { - gsap.from(footer, { - scrollTrigger: { - trigger: footer, - start: "top bottom", - }, - duration: 2, - opacity: 0, - yPercent: 10, - ease: "expo.out", + const mm = gsap.matchMedia(); + + mm.add("(prefers-reduced-motion: no-preference)", () => { + gsap.from(footer, { + scrollTrigger: { + trigger: footer, + start: "top bottom", + }, + duration: 2, + opacity: 0, + yPercent: 10, + // yPercent: 10, + ease: "expo.out", + }); }); } - \ No newline at end of file + diff --git a/src/components/Header.astro b/src/components/Header.astro index 63f6630..944f81c 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -82,14 +82,18 @@ import SkipNavLink from "./SkipNavLink.astro"; const header = document.querySelector("header"); if (header) { - const tl = gsap.timeline(); + const mm = gsap.matchMedia(); - tl.from(header.children, { - duration: 1, - yPercent: -120, - opacity: 0, - stagger: 0.2, - ease: "expo.out", + mm.add("(prefers-reduced-motion: no-preference)", () => { + const tl = gsap.timeline(); + + tl.from(header.children, { + duration: 1, + yPercent: -120, + opacity: 0, + stagger: 0.2, + ease: "expo.out", + }); }); } diff --git a/src/components/landing/AboutSection.astro b/src/components/landing/AboutSection.astro index 5f58e40..9bfc9b6 100644 --- a/src/components/landing/AboutSection.astro +++ b/src/components/landing/AboutSection.astro @@ -122,87 +122,91 @@ import { Picture } from "astro:assets"; const aboutSection = document.querySelector(".about"); if (aboutSection) { - const tl = gsap.timeline({ - scrollTrigger: { - trigger: aboutSection, - start: "top 70%", - }, - }); + const mm = gsap.matchMedia(); - tl.addLabel("heading", 0); - tl.addLabel("heading-text", 0.6); - tl.addLabel("image", 0.8); - tl.addLabel("image-text", 1.1); + mm.add("(prefers-reduced-motion: no-preference)", () => { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: aboutSection, + start: "top 70%", + }, + }); - new SplitText(aboutSection.querySelector("h2"), { - type: "words, 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", - ); - }, - }); + tl.addLabel("heading", 0); + tl.addLabel("heading-text", 0.6); + tl.addLabel("image", 0.8); + tl.addLabel("image-text", 1.1); - new SplitText(aboutSection.querySelectorAll(".heading 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(), - }, - "heading-text", - ); - }, - }); + new SplitText(aboutSection.querySelector("h2"), { + type: "words, 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", + ); + }, + }); - tl.from( - aboutSection.querySelector(".background"), - { - duration: 2, - opacity: 0, - ease: "expo.out", - }, - "image", - ); + new SplitText(aboutSection.querySelectorAll(".heading 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(), + }, + "heading-text", + ); + }, + }); - new SplitText(aboutSection.querySelectorAll(".image > 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(), - }, - "image-text", - ); - }, + tl.from( + aboutSection.querySelector(".background"), + { + duration: 2, + opacity: 0, + ease: "expo.out", + }, + "image", + ); + + new SplitText(aboutSection.querySelectorAll(".image > 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(), + }, + "image-text", + ); + }, + }); }); } diff --git a/src/components/landing/ContactSection.astro b/src/components/landing/ContactSection.astro index c540ec7..962c64a 100644 --- a/src/components/landing/ContactSection.astro +++ b/src/components/landing/ContactSection.astro @@ -104,100 +104,104 @@ import ArrowUpRight from "../icons/ArrowUpRight.astro"; const contactSection = document.querySelector(".contact"); if (contactSection) { - const tl = gsap.timeline({ - scrollTrigger: { - trigger: contactSection, - start: "top 60%", - }, - }); + const mm = gsap.matchMedia(); - tl.addLabel("image", 0.2); - tl.addLabel("heading", 0.5); - tl.addLabel("text", 0.6); - tl.addLabel("arrow", 0.6); + mm.add("(prefers-reduced-motion: no-preference)", () => { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: contactSection, + start: "top 60%", + }, + }); - new SplitText(contactSection.querySelector("h2"), { - type: "words, chars", - autoSplit: true, - mask: "chars", - charsClass: "char", - onSplit: (self) => { - tl.from( - self.chars, - { + tl.addLabel("image", 0.2); + tl.addLabel("heading", 0.5); + tl.addLabel("text", 0.6); + tl.addLabel("arrow", 0.6); + + new SplitText(contactSection.querySelector("h2"), { + type: "words, 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, 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, - yPercent: -120, - scale: 1.2, - stagger: 0.015, + tl.from( + contactSection.querySelector("a > svg"), + { + duration: 1, + opacity: 0, + yPercent: -10, ease: "expo.out", - onComplete: () => self.revert(), - }); - }, + }, + "arrow", + ); }); - - tl.from( - contactSection.querySelector("a > svg"), - { - duration: 1, - opacity: 0, - yPercent: -10, - ease: "expo.out", - }, - "arrow", - ); } diff --git a/src/components/landing/HeroSection.astro b/src/components/landing/HeroSection.astro index fac9dc9..5a0a423 100644 --- a/src/components/landing/HeroSection.astro +++ b/src/components/landing/HeroSection.astro @@ -125,74 +125,78 @@ import ArrowDown from "../icons/ArrowDown.astro"; const heroSection = document.querySelector(".hero"); if (heroSection) { - const tl = gsap.timeline().pause(); + const mm = gsap.matchMedia(); - tl.addLabel("heading", 0); - tl.addLabel("image", 0.2); - tl.addLabel("text", 0.8); - tl.addLabel("arrow", 1.5); + mm.add("(prefers-reduced-motion: no-preference)", () => { + const tl = gsap.timeline().pause(); - tl.from( - heroSection.querySelector(".image"), - { - duration: 2, - opacity: 0, - xPercent: 10, - ease: "expo.out", - }, - "image", - ); + tl.addLabel("heading", 0); + tl.addLabel("image", 0.2); + tl.addLabel("text", 0.8); + tl.addLabel("arrow", 1.5); - new SplitText(heroSection.querySelector("h1"), { - 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", - ); - }, + tl.from( + heroSection.querySelector(".image"), + { + duration: 2, + opacity: 0, + xPercent: 10, + ease: "expo.out", + }, + "image", + ); + + new SplitText(heroSection.querySelector("h1"), { + 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(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", - ); } diff --git a/src/components/landing/ServicesSection.astro b/src/components/landing/ServicesSection.astro index 30a2c05..4bbfadd 100644 --- a/src/components/landing/ServicesSection.astro +++ b/src/components/landing/ServicesSection.astro @@ -110,68 +110,72 @@ const services: Array> = [ const servicesSection = document.querySelector(".services"); if (servicesSection) { - const tl = gsap.timeline({ - scrollTrigger: { - trigger: servicesSection, - start: "top 60%", - }, + const mm = gsap.matchMedia(); + + mm.add("(prefers-reduced-motion: no-preference)", () => { + 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", - ); } diff --git a/src/components/landing/ValuesSection.astro b/src/components/landing/ValuesSection.astro index 1b7d2da..b508a5b 100644 --- a/src/components/landing/ValuesSection.astro +++ b/src/components/landing/ValuesSection.astro @@ -113,101 +113,105 @@ function formatIndex(index: number) { const valuesSection = document.querySelector(".values"); if (valuesSection) { - const heading = valuesSection.querySelector("h2"); + const mm = gsap.matchMedia(); - new SplitText(heading, { - type: "words, chars", - autoSplit: true, - mask: "chars", - charsClass: "char", - onSplit: (self) => { - gsap.from(self.chars, { + mm.add("(prefers-reduced-motion: no-preference)", () => { + const heading = valuesSection.querySelector("h2"); + + new SplitText(heading, { + type: "words, chars", + autoSplit: true, + 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: { - trigger: heading, + trigger: item, 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) => { - const itemTl = gsap.timeline({ - scrollTrigger: { - trigger: item, - start: "top 70%", - }, - }); + new SplitText(item.querySelector("span"), { + type: "words, chars", + autoSplit: true, + mask: "chars", + 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); - itemTl.addLabel("heading", 0.2); - itemTl.addLabel("text", 0.5); + 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("span"), { - type: "words, chars", - autoSplit: true, - mask: "chars", - charsClass: "char", - onSplit: (self) => { - itemTl.from( - self.chars, - { - duration: 0.9, - yPercent: -120, - stagger: 0.25, - ease: "expo.out", - onComplete: () => self.revert(), - }, - "number", - ); - }, - }); - - 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", - ); - }, + 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", + ); + }, + }); }); }); } diff --git a/src/pages/imprint.astro b/src/pages/imprint.astro index 7bd0fad..8bcf781 100644 --- a/src/pages/imprint.astro +++ b/src/pages/imprint.astro @@ -63,30 +63,34 @@ import BaseLayout from "../layouts/BaseLayout.astro"; const page = document.querySelector(".imprint"); if (page) { - new SplitText(page.querySelector("h1"), { - 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(), - }); - }, - }); + const mm = gsap.matchMedia(); - gsap.from(page.querySelectorAll(":scope > div"), { - duration: 1, - delay: 0.3, - opacity: 0, - yPercent: -20, - ease: "expo.out", - stagger: 0.1, + mm.add("(prefers-reduced-motion: no-preference)", () => { + new SplitText(page.querySelector("h1"), { + 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"), { + duration: 1, + delay: 0.3, + opacity: 0, + yPercent: -20, + ease: "expo.out", + stagger: 0.1, + }); }); } diff --git a/src/pages/old/blog.astro b/src/pages/old/blog.astro deleted file mode 100644 index 53ec016..0000000 --- a/src/pages/old/blog.astro +++ /dev/null @@ -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"; ---- - - -

This is where I will post about my journey learning Astro.

- -
diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro index cb44817..3b3dfaf 100644 --- a/src/pages/privacy.astro +++ b/src/pages/privacy.astro @@ -134,30 +134,34 @@ import BaseLayout from "../layouts/BaseLayout.astro"; const page = document.querySelector(".privacy"); if (page) { - new SplitText(page.querySelector("h1"), { - 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(), - }); - }, - }); + const mm = gsap.matchMedia(); - gsap.from(page.querySelectorAll(".content > *"), { - duration: 1, - delay: 0.3, - opacity: 0, - yPercent: -20, - ease: "expo.out", - stagger: 0.1, + mm.add("(prefers-reduced-motion: no-preference)", () => { + new SplitText(page.querySelector("h1"), { + 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 > *"), { + duration: 1, + delay: 0.3, + opacity: 0, + yPercent: -20, + ease: "expo.out", + stagger: 0.1, + }); }); }