mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-07-06 22:32:05 +08:00
Adds a new searchable `motion` domain (motion.csv, 16 GSAP animation skeletons across hover/scroll-reveal/stagger/page-transition/parallax/ loading, each tagged Subtle/Standard/Complex) plus three optional 1-10 dials on `--design-system` — --variance, --motion, --density — inspired by taste-skill's DESIGN_VARIANCE/MOTION_INTENSITY/VISUAL_DENSITY sliders. - --variance biases style search/selection toward centered-minimal (low) or bold-asymmetric (high) categories - --motion attaches a matching GSAP snippet (code + framework notes + do/don't) from motion.csv - --density overrides the --space-* token scale in ASCII/markdown/MASTER.md output (spacious <-> dense/dashboard) All three dials are optional and additive; output with no dials set is byte-identical to before (verified via regression diff). Synced into .claude/skills/ui-ux-pro-max and cli/assets alongside src/ui-ux-pro-max (source of truth) so all three installed copies stay consistent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
10 KiB
| 1 | No | Category | Intensity Tier | Keywords | Trigger | Duration | Easing | GSAP Snippet | Framework Notes | Do | Don't | Performance Notes |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 1 | Hover Micro-interaction | Subtle | hover, button, opacity, lift, press feedback | hover | 150-200ms | power1.out | gsap.to(el, { y: -1, opacity: 0.9, duration: 0.15, ease: 'power1.out' }); | Bind on mouseenter/mouseleave; in React wrap in a ref + useEffect (or onMouseEnter/onMouseLeave props directly calling gsap.to) | Keep displacement under 2px so it reads as feedback not motion | Don't animate layout-affecting props (width/height/margin) on hover | Runs on transform/opacity only so it stays on the compositor thread |
| 3 | 2 | Hover Micro-interaction | Standard | hover, card, scale, tilt, cursor feedback | hover | 200-300ms | power2.out | gsap.to(el, { y: -4, scale: 1.02, boxShadow: '0 12px 24px rgba(0,0,0,0.12)', duration: 0.25, ease: 'power2.out' }); | Use gsap.quickTo(el, 'y') for cards with many hover targets to avoid re-creating tweens every event | Pair with a matching mouseleave tween that reverses the same properties | Don't leave the hover state stuck if the pointer leaves fast; always attach the reverse tween | quickTo() avoids GC churn on lists with 20+ hoverable cards |
| 4 | 3 | Hover Micro-interaction | Complex | hover, magnetic, cursor follow, 3d tilt | hover + mousemove | 300-500ms | elastic.out(1,0.4) | const xTo = gsap.quickTo(el, 'x', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); const yTo = gsap.quickTo(el, 'y', { duration: 0.4, ease: 'elastic.out(1,0.4)' }); el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); xTo((e.clientX - r.left - r.width/2) * 0.3); yTo((e.clientY - r.top - r.height/2) * 0.3); }); | Debounce is not needed since quickTo interpolates; remove listeners on component unmount in React/Vue to avoid leaks | Clamp the pull strength (e.g. * 0.3) so the element never fully leaves its hit box | Don't apply magnetic effect to more than 1-2 focal elements per screen; it becomes noisy | Use will-change: transform on the target element for smoother compositing |
| 5 | 4 | Scroll Reveal | Subtle | scroll, fade in, reveal, on view | scroll (viewport enter) | 300-400ms | power1.out | gsap.from(el, { opacity: 0, y: 12, duration: 0.35, ease: 'power1.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reverse' } }); | Requires the ScrollTrigger plugin registered once via gsap.registerPlugin(ScrollTrigger) | Keep the y offset small (8-16px) so it reads as a fade, not a slide | Don't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallback | toggleActions 'play none none reverse' avoids re-triggering on every scroll direction change |
| 6 | 5 | Scroll Reveal | Standard | scroll, slide up, staggered section, reveal | scroll (viewport enter) | 400-600ms | power2.out | gsap.from(el.children, { opacity: 0, y: 24, duration: 0.5, stagger: 0.08, ease: 'power2.out', scrollTrigger: { trigger: el, start: 'top 85%' } }); | In React use useGSAP(() => {...}, { scope: containerRef }) from @gsap/react to auto-cleanup on unmount | Scope the ScrollTrigger to the section container so it doesn't re-scan the whole page | Don't stagger more than ~8 children; beyond that the last items feel laggy | Set scroller/markers: false in production; markers is dev-only |
| 7 | 6 | Scroll Reveal | Complex | scroll, pin, scrub, storytelling, scrollytelling | scroll (continuous scrub) | tied to scroll position | none (scrub-driven) | gsap.timeline({ scrollTrigger: { trigger: section, start: 'top top', end: '+=150%', scrub: 1, pin: true } }).from('.headline', { opacity: 0, y: 40 }).to('.bg-layer', { yPercent: -20 }, '<'); | Pinning needs the section to have deterministic height; recalc ScrollTrigger.refresh() after images/fonts load | Use scrub: true or a small number (0.5-1.5) instead of instant jumps so it feels tied to the scrollbar | Don't pin more than 1-2 sections per page; excessive pinning fights native scroll feel and hurts mobile UX | Pinning forces layout reflow; test on mid-tier mobile devices, not just desktop |
| 8 | 7 | Stagger List | Subtle | list, stagger, cards, grid entrance | load or scroll | 250-350ms | power1.out | gsap.from('.list-item', { opacity: 0, y: 8, duration: 0.3, stagger: 0.03 }); | Select items with a stable class/data-attribute (not array index) so re-renders in React don't break targeting | Keep per-item stagger delay small (0.02-0.04s) for lists longer than 10 items | Don't stagger by more than 0.1s per item on long lists; total reveal time becomes sluggish | For virtualized lists, only animate items currently mounted in the DOM |
| 9 | 8 | Stagger List | Standard | grid, bento, cards, staggered scale | load or scroll | 300-450ms | back.out(1.4) | gsap.from('.grid-item', { opacity: 0, scale: 0.92, y: 16, duration: 0.4, stagger: { each: 0.06, from: 'start', grid: 'auto' }, ease: 'back.out(1.4)' }); | grid: 'auto' lets GSAP infer rows/columns from a CSS grid layout for a natural wave stagger | Combine with from: 'center' for a bento-grid layout to draw the eye inward first | Don't use back.out on dense data tables; the overshoot reads as sloppy on informational UI | Group DOM writes; avoid interleaving layout reads (getBoundingClientRect) between staggered tweens |
| 10 | 9 | Stagger List | Complex | stagger, wave, text reveal, split text | load or scroll | 400-700ms | expo.out | const split = new SplitText(headline, { type: 'chars' }); gsap.from(split.chars, { opacity: 0, y: 20, rotateX: -40, duration: 0.6, stagger: 0.015, ease: 'expo.out' }); | SplitText is a GSAP Club/paid plugin; confirm license before shipping and provide a plain fade fallback if unavailable | Revert SplitText on unmount/cleanup (split.revert()) to restore original text nodes for accessibility tools | Don't split-animate long paragraphs; reserve for short headlines (under ~8 words) | Splitting text creates one element per character; keep it to headline-length copy only for DOM size |
| 11 | 10 | Page Transition | Subtle | route change, fade, page transition | route change | 200-300ms | power1.inOut | gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } }); | Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach) | Preload the destination route's critical assets before the exit tween finishes | Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive | Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy |
| 12 | 11 | Page Transition | Standard | route change, slide, overlay wipe | route change | 400-600ms | power2.inOut | const tl = gsap.timeline(); tl.to('.transition-overlay', { yPercent: 0, duration: 0.4, ease: 'power2.inOut' }).call(navigate).to('.transition-overlay', { yPercent: -100, duration: 0.4, ease: 'power2.inOut', delay: 0.1 }); | Keep the overlay element mounted at the layout root (outside the page component) so it survives the route swap | Show a lightweight loading indicator if the destination route's data fetch outlasts the overlay | Don't tie the overlay's reveal directly to data-fetch completion without a max-wait timeout; a slow API stalls the whole transition | Prefer CSS transform (yPercent) over top/left to keep the overlay animation on the compositor thread |
| 13 | 12 | Page Transition | Complex | shared element, morph, hero transition | route change | 500-800ms | expo.inOut | const state = Flip.getState('.hero-image'); navigate(); Flip.from(state, { duration: 0.6, ease: 'expo.inOut', absolute: true, zIndex: 100 }); | Requires the GSAP Flip plugin; the 'from' and 'to' route must render the same element with a shared data-flip-id | Verify the shared element exists in both DOM states before calling Flip.from to avoid a silent no-op | Don't use shared-element transitions across more than one element pair per navigation; compounding Flips are hard to time correctly | Flip recalculates layout (FLIP technique) so test on low-end devices for jank |
| 14 | 13 | Parallax Scroll | Subtle | parallax, background, depth, scroll speed | scroll (continuous) | tied to scroll position | linear (scrub) | gsap.to('.bg-layer', { yPercent: 10, ease: 'none', scrollTrigger: { trigger: section, scrub: true } }); | Apply parallax to background/decorative layers only, never to text or interactive controls | Keep the yPercent delta small (5-15) so foreground and background never desync distractingly | Don't parallax body copy; it hurts reading comfort and can trigger motion sickness | will-change: transform on the parallax layer only; remove it after scroll settles to free GPU memory |
| 15 | 14 | Parallax Scroll | Standard | multi-layer parallax, depth, hero background | scroll (continuous) | tied to scroll position | linear (scrub) | gsap.utils.toArray('.parallax-layer').forEach((layer, i) => { gsap.to(layer, { yPercent: (i + 1) * -8, ease: 'none', scrollTrigger: { trigger: layer.parentElement, scrub: 0.5 } }); }); | Layer count beyond 3-4 has diminishing visual return and multiplies scroll-listener cost | Vary speed per layer (background slowest, foreground fastest) to sell the depth illusion | Don't let parallax layers overflow their container; clip with overflow: hidden on the wrapper | Batch all layers under one ScrollTrigger container where possible instead of one per layer |
| 16 | 15 | Loading / Skeleton | Subtle | loading, skeleton, shimmer, pulse | on mount / async wait | 1200-1600ms loop | sine.inOut | gsap.to('.skeleton', { backgroundPosition: '200% 0', duration: 1.4, ease: 'sine.inOut', repeat: -1 }); | Kill the loop tween (tween.kill()) as soon as real content mounts to avoid orphaned repeating animations | Use a CSS gradient background-position sweep rather than opacity pulsing; reads as 'loading' more clearly | Don't run more than one shimmer loop per skeleton group; sync them under one timeline so the wave reads as a single unit | repeat: -1 tweens are cheap but must be explicitly killed on unmount or they leak in SPA route changes |
| 17 | 16 | Loading / Skeleton | Standard | progress, spinner, morphing loader | on mount / async wait | 800-1200ms loop | power1.inOut | gsap.timeline({ repeat: -1 }).to('.loader-dot', { y: -8, duration: 0.4, stagger: { each: 0.15, yoyo: true, repeat: 1 } }); | Wrap the whole loop timeline in useGSAP with { revertOnUpdate: false } in React so it isn't rebuilt every render | Cap total loop duration under ~1.5s so long waits don't feel like the UI froze on a single beat | Don't use elaborate loaders for sub-300ms waits; they flash and feel worse than no indicator | Pause the timeline (tl.pause()) when the loading tab/view is not visible to save CPU on background tabs |