diff --git a/skills/motion_ui/skill.md b/skills/motion_ui/skill.md index 8b137891..1303d56d 100644 --- a/skills/motion_ui/skill.md +++ b/skills/motion_ui/skill.md @@ -1 +1,488 @@ +--- + +name: motion-ui +description: "Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns." +-------------------------------------------------------------------------------------------------------------------------------------- + +# Motion System v4.1 + +Production-ready UI motion system for React / Next.js. + +Focused on **performance, accessibility, and usability** — not decoration. + +--- + +## When to Use + +Use this motion system when motion: + +* Guides attention (e.g., onboarding, key actions) +* Communicates state (loading, success, error, transitions) +* Preserves spatial continuity (layout changes, navigation) + +### Appropriate Scenarios + +* Interactive components (buttons, modals, menus) +* State transitions (loading → loaded, open → closed) +* Navigation and layout continuity (shared elements, crossfade) + +### Considerations + +* **Accessibility**: Always support reduced motion +* **Device adaptation**: Adjust for low-end devices +* **Performance trade-offs**: Prefer responsiveness over visual smoothness + +### Avoid Using Motion When + +* It is purely decorative +* It reduces usability or clarity +* It impacts performance negatively + +--- + +## How It Works + +### Core Principle + +Motion must: + +* Guide attention +* Communicate state +* Preserve spatial continuity + +If it does none → remove it. + +--- + +### Installation + +```bash +npm install motion +``` + +--- + +### Version + +* `motion/react` → default +* `framer-motion` → legacy + +Do not mix. + +--- + +### Motion Tokens + +```ts +export const motionTokens = { + duration: { + fast: 0.18, + normal: 0.35, + slow: 0.6 + }, + easing: { + smooth: [0.22, 1, 0.36, 1], + sharp: [0.4, 0, 0.2, 1] + }, + distance: { + sm: 8, + md: 16, + lg: 24 + } +} +``` + +--- + +### Performance Rules + +**Safe** + +* transform +* opacity + +**Avoid** + +* width / height +* top / left + +Rule: responsiveness > smoothness + +--- + +### Device Adaptation + +```ts +const isLowEnd = + typeof navigator !== "undefined" && + navigator.hardwareConcurrency <= 4 + +const duration = isLowEnd ? 0.2 : 0.4 +``` + +--- + +### Accessibility + +#### JS (useReducedMotion) + +```tsx +import { motion, useReducedMotion } from "motion/react" + +const reduce = useReducedMotion() + + +``` + +#### CSS + +```css +@media (prefers-reduced-motion: reduce) { + .motion-safe-transition { + transition: opacity 0.2s; + } + + .motion-reduce-transform { + transform: none !important; + } +} +``` + +#### Tailwind + +```html +
+``` + +--- + +### Architecture & Patterns + +#### Core Patterns + +* Hover → `whileHover` +* Tap → `whileTap` +* In view → `whileInView` +* Scroll linked → `useScroll` +* Conditional → `AnimatePresence` +* Layout small → `layout` +* Layout large → avoid +* Complex → `useAnimate` + +#### Layout & Transitions + +* Shared transitions → `layoutId` +* Presence transitions → `AnimatePresence` + +--- + +### Advanced Patterns (Concepts) + +* Parallax (scroll-linked transforms) +* Scroll storytelling (sticky sections) +* 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 + +* Focus trap +* Escape close +* Scroll lock +* ARIA roles + +#### Minimal Example + +```tsx +import { useEffect, useRef } from "react" +import { motion, AnimatePresence } from "motion/react" + +// Placeholder hooks (implement or replace with libraries) +function useFocusTrap(ref: React.RefObject, active: boolean) { + useEffect(() => { + if (!active || !ref.current) return + const el = ref.current + const focusable = el.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + const first = focusable[0] + const last = focusable[focusable.length - 1] + + function handleKey(e: KeyboardEvent) { + if (e.key !== "Tab") return + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); last?.focus() + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); first?.focus() + } + } + + el.addEventListener("keydown", handleKey) + first?.focus() + return () => el.removeEventListener("keydown", handleKey) + }, [active, ref]) +} + +function useScrollLock(active: boolean) { + useEffect(() => { + if (!active) return + const prev = document.body.style.overflow + document.body.style.overflow = "hidden" + return () => { document.body.style.overflow = prev } + }, [active]) +} + +export function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) { + const ref = useRef(null) + + useFocusTrap(ref, open) + useScrollLock(open) + + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") closeModal() + } + if (open) window.addEventListener("keydown", onKey) + return () => window.removeEventListener("keydown", onKey) + }, [open, closeModal]) + + return ( + + {open && ( + + + + + + )} + + ) +} + +// Usage +export function Example() { + const [open, setOpen] = React.useState(false) + const openModal = () => setOpen(true) + const closeModal = () => setOpen(false) + + return ( + <> + + + + ) +} +``` + +--- + +### SSR Safety + +* 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 +import { motion } from "motion/react" + +export function Button() { + return ( + + Click me + + ) +} +``` + +--- + +### Reduced Motion Example + +```tsx +import { motion, useReducedMotion } from "motion/react" + +export function FadeIn() { + const reduce = useReducedMotion() + + return ( + + ) +} +``` + +--- + +### Stagger List + +```tsx +import { motion } from "motion/react" + +const container = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.08 } + } +} + +const item = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0 } +} + +export function List() { + return ( + + {[1,2,3].map(i => ( + Item {i} + ))} + + ) +} +``` + +--- + +### Modal with AnimatePresence + +```tsx +import { motion, AnimatePresence } from "motion/react" + +export function Modal({ open }) { + return ( + + {open && ( + + )} + + ) +} +``` + +--- + +### Scroll Parallax + +```tsx +import { useScroll, useTransform, motion } from "motion/react" + +export function Parallax() { + const { scrollYProgress } = useScroll() + const y = useTransform(scrollYProgress, [0, 1], [0, -80]) + + return +} +``` + +--- + +### Skeleton Loading + +```tsx +import { motion } from "motion/react" + +export function Skeleton() { + return ( + + ) +} +``` + +--- + +### Shared Layout (Crossfade) + +```tsx +import { motion } from "motion/react" + +export function Shared() { + return +} +```