Revise motion UI documentation structure and content

Refactor motion UI documentation for clarity and consistency. Update sections on usage, scenarios, and examples.
This commit is contained in:
Jeff 2026-05-09 22:35:42 +05:30 committed by GitHub
parent 2fa8f59c34
commit 957d5e72f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,3 +1,4 @@
````md
--- ---
name: motion-ui name: motion-ui
description: "Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns." description: "Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns."
@ -11,66 +12,64 @@ Focused on **performance, accessibility, and usability** — not decoration.
**by Jatan** **by Jatan**
---
## When to Use ## When to Use
Use this motion system when motion: Use this motion system when motion:
* Guides attention (e.g., onboarding, key actions) - Guides attention (onboarding, primary actions)
* Communicates state (loading, success, error, transitions) - Communicates state (loading, success, error, transitions)
* Preserves spatial continuity (layout changes, navigation) - Preserves spatial continuity (navigation, layout changes)
### Appropriate Scenarios ### Appropriate Scenarios
* Interactive components (buttons, modals, menus) - Interactive UI (buttons, modals, menus)
* State transitions (loading → loaded, open → closed) - State transitions (open/close, loading states)
* Navigation and layout continuity (shared elements, crossfade) - Navigation transitions and shared elements
### Considerations ### Considerations
* **Accessibility**: Always support reduced motion - Accessibility must be preserved (reduced motion support)
* **Device adaptation**: Adjust for low-end devices - Low-end device performance must be respected
* **Performance trade-offs**: Prefer responsiveness over visual smoothness - Prefer responsiveness over visual smoothness
### Avoid Using Motion When ### Avoid Motion When
* It is purely decorative - It is purely decorative
* It reduces usability or clarity - It reduces clarity or usability
* It impacts performance negatively - It impacts performance
--- ---
## How It Works ## Core Principle
### Core Principle
Motion must: Motion must:
* Guide attention - Guide attention
* Communicate state - Communicate state
* Preserve spatial continuity - Preserve spatial continuity
If it does none → remove it. If it does none → remove it.
--- ---
### Installation ## Installation
```bash ```bash
npm install motion npm install motion
``` ````
--- ---
### Version ## Versions
* `motion/react` → default * `motion/react` → default
* `framer-motion` → legacy * `framer-motion` → legacy (do not mix)
Do not mix.
--- ---
### Motion Tokens ## Motion Tokens
```ts ```ts
export const motionTokens = { export const motionTokens = {
@ -93,23 +92,25 @@ export const motionTokens = {
--- ---
### Performance Rules ## Performance Rules
**Safe** ### Safe Properties
* transform * transform
* opacity * opacity
**Avoid** ### Avoid
* width / height * width
* top / left * height
* top
* left
Rule: responsiveness > smoothness Rule: responsiveness > smoothness
--- ---
### Device Adaptation ## Device Adaptation
```ts ```ts
const isLowEnd = const isLowEnd =
@ -121,22 +122,26 @@ const duration = isLowEnd ? 0.2 : 0.4
--- ---
### Accessibility ## Accessibility
#### JS (useReducedMotion) ### Reduced Motion (React)
```tsx ```tsx
import { motion, useReducedMotion } from "motion/react" import { motion, useReducedMotion } from "motion/react"
const reduce = useReducedMotion() const reduce = useReducedMotion()
<motion.div export function Example() {
initial={{ opacity: 0, y: reduce ? 0 : 24 }} return (
animate={{ opacity: 1, y: 0 }} <motion.div
/> initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
/>
)
}
``` ```
#### CSS ### CSS
```css ```css
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@ -150,7 +155,7 @@ const reduce = useReducedMotion()
} }
``` ```
#### Tailwind ### Tailwind
```html ```html
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div> <div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
@ -158,74 +163,79 @@ const reduce = useReducedMotion()
--- ---
### Architecture & Patterns ## Core Patterns
#### Core Patterns * hover → whileHover
* tap → whileTap
* Hover → `whileHover` * in-view → whileInView
* Tap → `whileTap` * scroll → useScroll
* In view → `whileInView` * conditional → AnimatePresence
* Scroll linked → `useScroll` * small layout → layout
* Conditional → `AnimatePresence` * large layout → avoid
* Layout small → `layout` * complex → useAnimate
* Layout large → avoid
* Complex → `useAnimate`
#### Layout & Transitions
* Shared transitions → `layoutId`
* Presence transitions → `AnimatePresence`
--- ---
### Advanced Patterns (Concepts) ## Layout System
* Parallax (scroll-linked transforms) * layoutId → shared transitions
* Scroll storytelling (sticky sections) * AnimatePresence → mount/unmount transitions
* 3D tilt (pointer-based transforms)
* Crossfade (shared layoutId)
* Progressive reveal (clip-path)
* Skeleton loading (looped opacity)
* Micro-interactions (hover/tap feedback)
* Spring system (physics-based motion)
--- ---
### Modal Essentials ## Advanced Patterns
* Focus trap * Parallax scrolling
* Escape close * Scroll storytelling sections
* Scroll lock * 3D pointer tilt
* ARIA roles * Crossfade transitions
* Clip-path reveals
* Skeleton loading loops
* Micro-interactions
* Spring physics motion
#### Minimal Example ---
## Modal System (Production Safe)
```tsx ```tsx
import { useEffect, useRef } from "react" import { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react" import { motion, AnimatePresence } from "motion/react"
// Placeholder hooks (implement or replace with libraries) type ModalProps = {
open: boolean
onClose: () => void
}
function useFocusTrap(ref: React.RefObject<HTMLElement>, active: boolean) { function useFocusTrap(ref: React.RefObject<HTMLElement>, active: boolean) {
useEffect(() => { useEffect(() => {
if (!active || !ref.current) return if (!active || !ref.current) return
const el = ref.current const el = ref.current
const focusable = el.querySelectorAll<HTMLElement>( const focusable = el.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) )
const first = focusable[0] const first = focusable[0]
const last = focusable[focusable.length - 1] const last = focusable[focusable.length - 1]
function handleKey(e: KeyboardEvent) { if (first) first.focus()
const handleKey = (e: KeyboardEvent) => {
if (e.key !== "Tab") return if (e.key !== "Tab") return
if (!first || !last) return
if (e.shiftKey && document.activeElement === first) { if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last?.focus() e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) { } else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first?.focus() e.preventDefault()
first.focus()
} }
} }
el.addEventListener("keydown", handleKey) el.addEventListener("keydown", handleKey)
first?.focus()
return () => el.removeEventListener("keydown", handleKey) return () => el.removeEventListener("keydown", handleKey)
}, [active, ref]) }, [active, ref])
} }
@ -233,25 +243,31 @@ function useFocusTrap(ref: React.RefObject<HTMLElement>, active: boolean) {
function useScrollLock(active: boolean) { function useScrollLock(active: boolean) {
useEffect(() => { useEffect(() => {
if (!active) return if (!active) return
const prev = document.body.style.overflow const prev = document.body.style.overflow
document.body.style.overflow = "hidden" document.body.style.overflow = "hidden"
return () => { document.body.style.overflow = prev }
return () => {
document.body.style.overflow = prev
}
}, [active]) }, [active])
} }
export function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) { export function Modal({ open, onClose }: ModalProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useFocusTrap(ref, open) useFocusTrap(ref, open)
useScrollLock(open) useScrollLock(open)
useEffect(() => { useEffect(() => {
function onKey(e: KeyboardEvent) { const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") closeModal() if (e.key === "Escape") onClose()
} }
if (open) window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey) if (open) window.addEventListener("keydown", onKeyDown)
}, [open, closeModal])
return () => window.removeEventListener("keydown", onKeyDown)
}, [open, onClose])
return ( return (
<AnimatePresence> <AnimatePresence>
@ -259,6 +275,7 @@ export function Modal({ open, closeModal }: { open: boolean; closeModal: () => v
<motion.div <motion.div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Modal"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
@ -271,118 +288,43 @@ export function Modal({ open, closeModal }: { open: boolean; closeModal: () => v
exit={{ scale: 0.95, opacity: 0 }} exit={{ scale: 0.95, opacity: 0 }}
className="bg-white p-6 rounded" className="bg-white p-6 rounded"
> >
<button onClick={closeModal}>Close</button> <button onClick={onClose}>Close</button>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
) )
} }
```
// Usage ---
export function Example() {
const [open, setOpen] = React.useState(false)
const openModal = () => setOpen(true)
const closeModal = () => setOpen(false)
return ( ## Scroll Parallax
<>
<button onClick={openModal}>Open</button> ```tsx
<Modal open={open} closeModal={closeModal} /> import { useScroll, useTransform, motion } from "motion/react"
</>
) export function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return <motion.div style={{ y }} />
} }
``` ```
--- ---
### SSR Safety ## Skeleton Loading
* Match initial states
* Avoid implicit animation origins
---
### Debugging
Check:
* Wrong import
* Missing `"use client"`
* Missing `key`
* Hydration mismatch
* Layout misuse
* State-driven animation
---
### QA
* No CLS
* Keyboard works
* Focus trapped
* ARIA correct
* Reduced motion works
* No hydration warnings
* Animations stop on unmount
---
### Anti-Patterns
* Animating layout properties
* Infinite animations without purpose
* Over-staggering lists
* Ignoring reduced motion
* Using motion for decoration
---
### Philosophy
Motion is interaction design.
---
### Final Rule
> If motion does not improve UX → remove it.
---
## Examples
### Button Interaction
```tsx ```tsx
import { motion } from "motion/react" import { motion } from "motion/react"
export function Button() { export function Skeleton() {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
>
Click me
</motion.button>
)
}
```
---
### Reduced Motion Example
```tsx
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }} className="bg-gray-200 h-6 w-full"
animate={{ opacity: 1, y: 0 }} animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ repeat: Infinity, duration: 1.2 }}
/> />
) )
} }
@ -390,7 +332,19 @@ export function FadeIn() {
--- ---
### Stagger List ## Shared Layout
```tsx
import { motion } from "motion/react"
export function Shared() {
return <motion.div layoutId="shared" />
}
```
---
## Stagger List
```tsx ```tsx
import { motion } from "motion/react" import { motion } from "motion/react"
@ -410,8 +364,10 @@ const item = {
export function List() { export function List() {
return ( return (
<motion.ul variants={container} initial="hidden" animate="visible"> <motion.ul variants={container} initial="hidden" animate="visible">
{[1,2,3].map(i => ( {[1, 2, 3].map(i => (
<motion.li key={i} variants={item}>Item {i}</motion.li> <motion.li key={i} variants={item}>
Item {i}
</motion.li>
))} ))}
</motion.ul> </motion.ul>
) )
@ -420,68 +376,37 @@ export function List() {
--- ---
### Modal with AnimatePresence ## Debug Checklist
```tsx * correct import (`motion/react`)
import { motion, AnimatePresence } from "motion/react" * `"use client"` in Next.js
* no missing keys
export function Modal({ open }) { * no layout shift (CLS)
return ( * no hydration mismatch
<AnimatePresence> * reduced motion works
{open && ( * keyboard navigation works
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
/>
)}
</AnimatePresence>
)
}
```
--- ---
### Scroll Parallax ## Anti-Patterns
```tsx * animating layout (width/height)
import { useScroll, useTransform, motion } from "motion/react" * decorative motion
* infinite motion without purpose
export function Parallax() { * ignoring reduced motion
const { scrollYProgress } = useScroll() * over-staggering lists
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return <motion.div style={{ y }} />
}
```
--- ---
### Skeleton Loading ## Philosophy
```tsx Motion is interaction design.
import { motion } from "motion/react"
export function Skeleton() {
return (
<motion.div
className="bg-gray-200 h-6 w-full"
animate={{ opacity: [0.6, 1, 0.6] }}
transition={{ repeat: Infinity }}
/>
)
}
```
--- ---
### Shared Layout (Crossfade) ## Final Rule
```tsx > If motion does not improve UX → remove it.
import { motion } from "motion/react"
export function Shared() {
return <motion.div layoutId="shared" />
}
``` ```
```