docs: fix motion skill examples

Fix copied example issues from the adopted #1780 motion skills: live reduced-motion config, tokenized distances/easing/springs, valid shimmer skeleton JSX, and visibility cleanup.
This commit is contained in:
Affaan Mustafa 2026-05-12 01:35:35 -04:00 committed by Affaan Mustafa
parent f219a90f20
commit cb2a70ce72
3 changed files with 96 additions and 51 deletions

View File

@ -278,7 +278,7 @@ import { motionTokens } from "@/lib/motion-tokens"
d="M 0 100 Q 50 0 100 100"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: motionTokens.duration.slow, ease: "easeInOut" }}
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>
```
@ -336,12 +336,13 @@ const { ref, style } = useScrollReveal()
"use client"
import { useEffect } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"
export function CursorFollower() {
const x = useMotionValue(-100)
const y = useMotionValue(-100)
const sx = useSpring(x, { stiffness: 120, damping: 16 })
const sy = useSpring(y, { stiffness: 120, damping: 16 })
const sx = useSpring(x, springs.gentle)
const sy = useSpring(y, springs.gentle)
useEffect(() => {
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
@ -363,29 +364,44 @@ export function CursorFollower() {
```tsx
"use client"
import { motion } from "motion/react"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
export function ShimmerSkeleton({ className }: { className?: string }) {
export function ShimmerSkeleton({ className = "" }: { className?: string }) {
const controls = useAnimation()
useEffect(() => {
const run = () =>
controls.start({ x: ["-100%", "100%"], transition: { repeat: Infinity, duration: 1.2, ease: "linear" } })
const onVis = () => (document.visibilityState === "hidden" ? controls.stop() : run())
run()
document.addEventListener("visibilitychange", onVis)
return () => document.removeEventListener("visibilitychange", onVis)
const play = () =>
controls.start({
x: ["-100%", "100%"],
transition: {
repeat: Infinity,
duration: motionTokens.duration.crawl,
ease: motionTokens.easing.linear,
},
})
const handleVisibility = () => {
if (document.visibilityState === "hidden") controls.stop()
else void play()
}
void play()
document.addEventListener("visibilitychange", handleVisibility)
return () => {
controls.stop()
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [controls])
return (
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
animate={{ x: ["-100%", "100%"] }}
transition={{ repeat: Infinity, duration: 1.2, ease: "linear" }}
return (
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
initial={{ x: "-100%" }}
animate={controls}
/>
</div>
)
}
/>
</div>
)
}
@ -443,8 +459,9 @@ export function LoadingButton({
```tsx
"use client"
import { useEffect, useRef } from "react"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
export function PulseDot() {
const controls = useAnimation()
@ -454,19 +471,23 @@ export function PulseDot() {
controls.start({
scale: [1, 1.4, 1],
opacity: [1, 0.6, 1],
transition: { repeat: Infinity, duration: 1.8 },
transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
})
// Rule 2: pause when tab is hidden
const handleVisibility = () => {
if (document.visibilityState === "hidden") controls.stop()
else pulse()
else void pulse()
}
pulse()
void pulse()
document.addEventListener("visibilitychange", handleVisibility)
return () => document.removeEventListener("visibilitychange", handleVisibility) // Rule 7
}, [])
// Rule 7: stop controls and remove listeners on unmount.
return () => {
controls.stop()
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [controls])
return <motion.span className="w-2 h-2 rounded-full bg-green-400" animate={controls} />
}

View File

@ -27,7 +27,7 @@ This skill produces:
- A shared `motionTokens` object (duration, easing, distance, scale)
- A shared `springs` preset map (5 named configs)
- A `shouldAnimate` boolean gate used by all components
- A `shouldAnimate()` gate used by all components
- Accessibility-compliant animation defaults via `useReducedMotion`
- SSR-safe initial states with zero hydration warnings
@ -79,7 +79,7 @@ These are non-negotiable. They apply to every component in the system.
### When to disable animation entirely
Disable (set `shouldAnimate = false`) when:
Disable (make `shouldAnimate()` return `false`) when:
- `prefersReduced` is `true`
- `isLowEnd` is `true` and the animation is non-essential
@ -134,20 +134,28 @@ export const springs = {
```ts
// lib/motion-config.ts
export const motionConfig = {
isLowEnd:
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4,
prefersReduced:
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
get shouldAnimate() {
return !this.prefersReduced
isLowEnd() {
return (
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4
)
},
get duration() {
return this.isLowEnd || this.prefersReduced
prefersReduced() {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
},
shouldAnimate({ essential = false } = {}) {
if (this.prefersReduced()) return false
if (!essential && this.isLowEnd()) return false
return true
},
duration() {
return this.isLowEnd() || this.prefersReduced()
? motionTokens.duration.instant
: motionTokens.duration.normal
},
@ -240,7 +248,7 @@ export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Device gate — skip animation on low-end hardware
if (!motionConfig.shouldAnimate || !mounted) {
if (!motionConfig.shouldAnimate() || !mounted) {
return <div>{children}</div>
}

View File

@ -141,7 +141,7 @@ const item = {
```tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { springs } from "@/lib/motion-tokens"
import { motionTokens, springs } from "@/lib/motion-tokens"
// Wrap at the call site:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
@ -164,9 +164,17 @@ export function Modal({ onClose }: { onClose: () => void }) {
role="dialog"
aria-modal="true"
className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
initial={{ opacity: 0, scale: 0.95, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 8 }}
initial={{
opacity: 0,
scale: motionTokens.scale.press,
y: motionTokens.distance.sm,
}}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{
opacity: 0,
scale: motionTokens.scale.press,
y: motionTokens.distance.sm,
}}
transition={springs.gentle}
/>
</>
@ -179,16 +187,24 @@ export function Modal({ onClose }: { onClose: () => void }) {
```tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { springs } from "@/lib/motion-tokens"
import { motionTokens, springs } from "@/lib/motion-tokens"
<AnimatePresence mode="sync">
{toasts.map((t) => (
<motion.div
key={t.id}
layout
initial={{ opacity: 0, x: 48, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 48, scale: 0.9 }}
initial={{
opacity: 0,
x: motionTokens.distance.xl,
scale: motionTokens.scale.subtle,
}}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{
opacity: 0,
x: motionTokens.distance.xl,
scale: motionTokens.scale.subtle,
}}
transition={springs.snappy}
/>
))}
@ -317,7 +333,7 @@ export function ExpandingCard({ title, body }: { title: string; body: string })
initial={false}
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
style={{ transformOrigin: "top", overflow: "hidden" }}
transition={{
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth,
}}