Files
文敏華 bd9329c9cb Add motion domain and design dials to design-system generator
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>
2026-07-01 22:54:02 +08:00

10 KiB

1NoCategoryIntensity TierKeywordsTriggerDurationEasingGSAP SnippetFramework NotesDoDon'tPerformance Notes
21Hover Micro-interactionSubtlehover, button, opacity, lift, press feedbackhover150-200mspower1.outgsap.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 motionDon't animate layout-affecting props (width/height/margin) on hoverRuns on transform/opacity only so it stays on the compositor thread
32Hover Micro-interactionStandardhover, card, scale, tilt, cursor feedbackhover200-300mspower2.outgsap.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 eventPair with a matching mouseleave tween that reverses the same propertiesDon't leave the hover state stuck if the pointer leaves fast; always attach the reverse tweenquickTo() avoids GC churn on lists with 20+ hoverable cards
43Hover Micro-interactionComplexhover, magnetic, cursor follow, 3d tilthover + mousemove300-500mselastic.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 leaksClamp the pull strength (e.g. * 0.3) so the element never fully leaves its hit boxDon't apply magnetic effect to more than 1-2 focal elements per screen; it becomes noisyUse will-change: transform on the target element for smoother compositing
54Scroll RevealSubtlescroll, fade in, reveal, on viewscroll (viewport enter)300-400mspower1.outgsap.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 slideDon't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallbacktoggleActions 'play none none reverse' avoids re-triggering on every scroll direction change
65Scroll RevealStandardscroll, slide up, staggered section, revealscroll (viewport enter)400-600mspower2.outgsap.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 unmountScope the ScrollTrigger to the section container so it doesn't re-scan the whole pageDon't stagger more than ~8 children; beyond that the last items feel laggySet scroller/markers: false in production; markers is dev-only
76Scroll RevealComplexscroll, pin, scrub, storytelling, scrollytellingscroll (continuous scrub)tied to scroll positionnone (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 loadUse scrub: true or a small number (0.5-1.5) instead of instant jumps so it feels tied to the scrollbarDon't pin more than 1-2 sections per page; excessive pinning fights native scroll feel and hurts mobile UXPinning forces layout reflow; test on mid-tier mobile devices, not just desktop
87Stagger ListSubtlelist, stagger, cards, grid entranceload or scroll250-350mspower1.outgsap.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 targetingKeep per-item stagger delay small (0.02-0.04s) for lists longer than 10 itemsDon't stagger by more than 0.1s per item on long lists; total reveal time becomes sluggishFor virtualized lists, only animate items currently mounted in the DOM
98Stagger ListStandardgrid, bento, cards, staggered scaleload or scroll300-450msback.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 staggerCombine with from: 'center' for a bento-grid layout to draw the eye inward firstDon't use back.out on dense data tables; the overshoot reads as sloppy on informational UIGroup DOM writes; avoid interleaving layout reads (getBoundingClientRect) between staggered tweens
109Stagger ListComplexstagger, wave, text reveal, split textload or scroll400-700msexpo.outconst 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 unavailableRevert SplitText on unmount/cleanup (split.revert()) to restore original text nodes for accessibility toolsDon'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
1110Page TransitionSubtleroute change, fade, page transitionroute change200-300mspower1.inOutgsap.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 finishesDon't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsiveExit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy
1211Page TransitionStandardroute change, slide, overlay wiperoute change400-600mspower2.inOutconst 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 swapShow a lightweight loading indicator if the destination route's data fetch outlasts the overlayDon't tie the overlay's reveal directly to data-fetch completion without a max-wait timeout; a slow API stalls the whole transitionPrefer CSS transform (yPercent) over top/left to keep the overlay animation on the compositor thread
1312Page TransitionComplexshared element, morph, hero transitionroute change500-800msexpo.inOutconst 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-idVerify the shared element exists in both DOM states before calling Flip.from to avoid a silent no-opDon't use shared-element transitions across more than one element pair per navigation; compounding Flips are hard to time correctlyFlip recalculates layout (FLIP technique) so test on low-end devices for jank
1413Parallax ScrollSubtleparallax, background, depth, scroll speedscroll (continuous)tied to scroll positionlinear (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 controlsKeep the yPercent delta small (5-15) so foreground and background never desync distractinglyDon't parallax body copy; it hurts reading comfort and can trigger motion sicknesswill-change: transform on the parallax layer only; remove it after scroll settles to free GPU memory
1514Parallax ScrollStandardmulti-layer parallax, depth, hero backgroundscroll (continuous)tied to scroll positionlinear (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 costVary speed per layer (background slowest, foreground fastest) to sell the depth illusionDon't let parallax layers overflow their container; clip with overflow: hidden on the wrapperBatch all layers under one ScrollTrigger container where possible instead of one per layer
1615Loading / SkeletonSubtleloading, skeleton, shimmer, pulseon mount / async wait1200-1600ms loopsine.inOutgsap.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 animationsUse a CSS gradient background-position sweep rather than opacity pulsing; reads as 'loading' more clearlyDon't run more than one shimmer loop per skeleton group; sync them under one timeline so the wave reads as a single unitrepeat: -1 tweens are cheap but must be explicitly killed on unmount or they leak in SPA route changes
1716Loading / SkeletonStandardprogress, spinner, morphing loaderon mount / async wait800-1200ms looppower1.inOutgsap.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 renderCap total loop duration under ~1.5s so long waits don't feel like the UI froze on a single beatDon't use elaborate loaders for sub-300ms waits; they flash and feel worse than no indicatorPause the timeline (tl.pause()) when the loading tab/view is not visible to save CPU on background tabs