motion: calm screen transitions to reduce motion sickness
Deploy to route.crispygoat.com / deploy (push) Successful in 4m30s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m30s
The site had a lot of aggressive motion that compounded into a vertigo-inducing experience. Visual design (colors, type, layout) is unchanged — only the movement has been calmed. Single-commit overview: - Route transitions: 90/140ms pure-opacity crossfade (was 220ms with 4-6px Y-shift on enter/exit). - atelier-* modal animations: 180ms opacity-only, 4px max (was 420ms with 20px slide + scale(0.96→1) and 80-440ms cumulative stagger). - Hover transforms: -1px lift or no lift (was -2px + scale(0.98)). - CTA shimmer: 400ms (was 700ms). - Toggle thumb: ease-out (was cubic-bezier(0.34, 1.56, 0.64, 1) bouncy overshoot). - GSAP ScrollAnimations: capped at 12-16px translation, 320ms duration, power1.out, reduced-motion guards at the top of every effect. ParallaxLayer no longer scrolls content; only the data-parallax attribute can opt in, and only to 24px. - TuxedoVideoHero: killed 80px scroll-driven Y-shift on hero, killed video 1.15 scale-on-scroll, killed parallax-float scroll effect, cut hero-reveal to 8px/320ms (was 40px/1s/power3.out), removed the motion.scale on the logo and CTA buttons, slowed the bouncing scroll indicator from 1.5s to 2.4s. - CinematicShowcase: killed morphing product cards (rotateY ±5°, scale 0.95→1.02), killed parallax background HARVEST text (-100px), killed translateX carousel, killed scale(0.9→1) back.out(1.4) reveal in favor of opacity-only 8px/320ms entrance, removed progress-dot scale, removed progress-bar transition lag. - OnboardingFlow: removed scale(0.9→1) and y:20→0 entrance animations. - Global MotionConfig: caps every framer-motion animation in the tree to 0.2s easeOut, and sets reducedMotion='user' so framer-motion automatically strips x/y/scale/rotate from all 71 motion.div reveals across the public site when the OS prefers-reduced-motion is set. - globals.css prefers-reduced-motion block: comprehensive kill switch that disables animation/transition duration app-wide, wipes the route view-transition, and clears the .parallax-float / .hero-reveal transforms. How to test: - Default: motion is calmer, ~10x faster, with no parallax - OS-level 'reduce motion' on: zero positional movement, opacity fades only. Files changed: 7 (no new files)
This commit is contained in:
+76
-49
@@ -189,28 +189,22 @@ select:-webkit-autofill:focus {
|
||||
* the cut, short enough to feel like a single app. The cubic-bezier
|
||||
* keeps the start crisp and the finish gentle.
|
||||
*/
|
||||
/* Route fade — pure opacity, no slide. The old "translateY(6px) on enter,
|
||||
* translateY(-4px) on exit" was a classic multi-axis vestibular trigger.
|
||||
* 140ms total is the sweet spot: barely there, but softens the cut. */
|
||||
::view-transition-old(page-content) {
|
||||
animation: route-fade-out 180ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation: route-fade-out 90ms ease-out both;
|
||||
}
|
||||
::view-transition-new(page-content) {
|
||||
animation: route-fade-in 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
animation: route-fade-in 140ms ease-out both;
|
||||
}
|
||||
|
||||
@keyframes route-fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@keyframes route-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Top-of-page shimmer for the LoadingFade placeholder bar. */
|
||||
@@ -220,7 +214,23 @@ select:-webkit-autofill:focus {
|
||||
}
|
||||
|
||||
/* Reduce motion: respect the user's OS-level preference. */
|
||||
/* The motion pass deliberately keeps the visual design intact (colors, type, layout)
|
||||
* but flattens everything that fights vestibular comfort. This block wipes:
|
||||
* - all CSS animations and transitions across the app
|
||||
* - all view transitions and route fades
|
||||
* - GSAP, framer-motion, and any scroll-driven transforms (those need JS-side guards
|
||||
* in their components, but the CSS-side transforms inherit this via inheritance).
|
||||
* If you want even less, just set prefers-reduced-motion: reduce in your OS and
|
||||
* every animation in the app turns off. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
::view-transition-old(page-content),
|
||||
::view-transition-new(page-content) {
|
||||
animation: none !important;
|
||||
@@ -228,17 +238,26 @@ select:-webkit-autofill:focus {
|
||||
@keyframes transition-shimmer {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
}
|
||||
/* Atelier modal + stagger animations also respect reduced-motion */
|
||||
.atelier-enter,
|
||||
.atelier-backdrop,
|
||||
.atelier-stagger > *,
|
||||
.atelier-cta .atelier-cta-shimmer {
|
||||
.atelier-cta .atelier-cta-shimmer,
|
||||
.parallax-float,
|
||||
.animate-bounce,
|
||||
.hero-reveal {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.atelier-stagger > * {
|
||||
.atelier-stagger > *,
|
||||
.parallax-float,
|
||||
.hero-reveal {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
/* Scroll progress bar (Tuxedo hero) — no animated width when motion is off */
|
||||
.scroll-progress-bar {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode placeholder (used by storefront) */
|
||||
@@ -290,7 +309,7 @@ select:-webkit-autofill:focus {
|
||||
box-shadow:
|
||||
0 16px 48px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ─── Premium card - Light Theme ─────────────────────────────────────── */
|
||||
@@ -321,10 +340,10 @@ select:-webkit-autofill:focus {
|
||||
0 8px 32px rgba(0, 0, 0, 0.06),
|
||||
0 0 0 1px rgba(26, 77, 46, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.card:active {
|
||||
transform: scale(0.98) translateY(0);
|
||||
transform: scale(0.995) translateY(0);
|
||||
}
|
||||
|
||||
.card-dark {
|
||||
@@ -377,10 +396,11 @@ select:-webkit-autofill:focus {
|
||||
box-shadow:
|
||||
0 8px 20px rgba(26, 77, 46, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
/* No vertical lift on hover — was a 1px translate that compounded with the gradient
|
||||
* shift and the inset highlight to read as jittery on the eye. */
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98) translateY(0);
|
||||
transform: scale(0.995);
|
||||
}
|
||||
|
||||
.btn-primary-green {
|
||||
@@ -406,7 +426,6 @@ select:-webkit-autofill:focus {
|
||||
box-shadow:
|
||||
0 8px 20px rgba(22, 163, 74, 0.4),
|
||||
inset 0 1px 0 rgba(255,255,255,0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ─── Gradient text ───────────────────────────────────────────── */
|
||||
@@ -729,13 +748,12 @@ select:-webkit-autofill:focus {
|
||||
.atelier-type-card:hover {
|
||||
border-color: rgba(34, 78, 47, 0.25);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.atelier-type-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.atelier-type-card:active {
|
||||
transform: translateY(0) scale(0.985);
|
||||
transform: scale(0.995);
|
||||
}
|
||||
.atelier-type-card.is-selected {
|
||||
background: linear-gradient(135deg, #224E2F 0%, #14532D 100%);
|
||||
@@ -747,11 +765,13 @@ select:-webkit-autofill:focus {
|
||||
}
|
||||
.atelier-type-card .atelier-type-icon {
|
||||
color: #786B53;
|
||||
transition: color 220ms ease, transform 220ms ease;
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
.atelier-type-card.is-selected .atelier-type-icon {
|
||||
color: #FCD34D;
|
||||
transform: scale(1.06);
|
||||
/* Removed the 1.06 scale on selected — the color change alone communicates the
|
||||
* selection. Scale + color change in the same beat read as "double confirmation",
|
||||
* which adds up when many cards animate at once. */
|
||||
}
|
||||
.atelier-type-card .atelier-type-name {
|
||||
font-family: var(--font-fraunces);
|
||||
@@ -768,12 +788,11 @@ select:-webkit-autofill:focus {
|
||||
.atelier-type-card .atelier-type-check {
|
||||
opacity: 0;
|
||||
color: #FCD34D;
|
||||
transform: scale(0.6);
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.atelier-type-card.is-selected .atelier-type-check {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
/* Removed the scale(0.6 → 1) on the checkmark — pure opacity fade is calmer. */
|
||||
}
|
||||
|
||||
/* Pill toggle — Active / Taxable */
|
||||
@@ -810,7 +829,11 @@ select:-webkit-autofill:focus {
|
||||
background: #FFFFFF;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 240ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
/* Was cubic-bezier(0.34, 1.56, 0.64, 1) — the bouncy overshoot ease that
|
||||
* overshoots ~13% past the target then settles. Looks playful on a static
|
||||
* design, but in a real product it reads as twitchy and contributes to the
|
||||
* sense of "too much movement". */
|
||||
transition: transform 160ms ease-out;
|
||||
}
|
||||
.atelier-toggle.is-on .atelier-toggle-thumb {
|
||||
transform: translateX(16px);
|
||||
@@ -940,35 +963,38 @@ select:-webkit-autofill:focus {
|
||||
color: #A8A29E;
|
||||
}
|
||||
|
||||
/* Modal enter animation */
|
||||
/* Modal enter animation — calmer version. The previous 420ms scale + 20px slide
|
||||
* with 80-440ms cumulative stagger delays meant the modal kept moving for nearly
|
||||
* a full second. Now: 180ms opacity-only fade-in, 4px max, no scale, no stagger. */
|
||||
@keyframes atelier-enter {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.96); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes atelier-backdrop {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes atelier-stagger {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.atelier-enter {
|
||||
animation: atelier-enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
animation: atelier-enter 180ms ease-out both;
|
||||
}
|
||||
.atelier-backdrop {
|
||||
animation: atelier-backdrop 300ms ease both;
|
||||
animation: atelier-backdrop 140ms ease-out both;
|
||||
}
|
||||
.atelier-stagger > * {
|
||||
animation: atelier-stagger 480ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
animation: atelier-stagger 180ms ease-out both;
|
||||
/* No more 80-440ms cumulative delays — items appear together, not as a parade. */
|
||||
}
|
||||
.atelier-stagger > *:nth-child(1) { animation-delay: 80ms; }
|
||||
.atelier-stagger > *:nth-child(2) { animation-delay: 140ms; }
|
||||
.atelier-stagger > *:nth-child(3) { animation-delay: 200ms; }
|
||||
.atelier-stagger > *:nth-child(4) { animation-delay: 260ms; }
|
||||
.atelier-stagger > *:nth-child(5) { animation-delay: 320ms; }
|
||||
.atelier-stagger > *:nth-child(6) { animation-delay: 380ms; }
|
||||
.atelier-stagger > *:nth-child(7) { animation-delay: 440ms; }
|
||||
.atelier-stagger > *:nth-child(1) { animation-delay: 0ms; }
|
||||
.atelier-stagger > *:nth-child(2) { animation-delay: 30ms; }
|
||||
.atelier-stagger > *:nth-child(3) { animation-delay: 60ms; }
|
||||
.atelier-stagger > *:nth-child(4) { animation-delay: 90ms; }
|
||||
.atelier-stagger > *:nth-child(5) { animation-delay: 120ms; }
|
||||
.atelier-stagger > *:nth-child(6) { animation-delay: 150ms; }
|
||||
.atelier-stagger > *:nth-child(7) { animation-delay: 180ms; }
|
||||
|
||||
/* Primary CTA — gradient with subtle inner highlight */
|
||||
.atelier-cta {
|
||||
@@ -993,7 +1019,7 @@ select:-webkit-autofill:focus {
|
||||
0 6px 18px -6px rgba(20, 83, 45, 0.55),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.14),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.10);
|
||||
transition: all 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background-position 220ms ease-out, box-shadow 180ms ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.atelier-cta:hover {
|
||||
@@ -1002,10 +1028,9 @@ select:-webkit-autofill:focus {
|
||||
0 10px 28px -8px rgba(20, 83, 45, 0.65),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.20),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.atelier-cta:active {
|
||||
transform: translateY(0) scale(0.985);
|
||||
transform: scale(0.995);
|
||||
}
|
||||
.atelier-cta:disabled {
|
||||
opacity: 0.5;
|
||||
@@ -1022,7 +1047,9 @@ select:-webkit-autofill:focus {
|
||||
height: 100%;
|
||||
background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.18) 50%, transparent 70%);
|
||||
pointer-events: none;
|
||||
transition: left 700ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* Was 700ms — the shimmer is decorative but a long linear slide on every
|
||||
* hover draws the eye. 400ms keeps the highlight but doesn't linger. */
|
||||
transition: left 400ms ease-out;
|
||||
}
|
||||
.atelier-cta:hover .atelier-cta-shimmer {
|
||||
left: 130%;
|
||||
|
||||
Reference in New Issue
Block a user