Skill de Motion Design para animações, transições e micro-interações. Use quando precisar definir ou implementar animações de interface, transições entre páginas, efeitos de hover/click/focus, loading states animados, ou qualquer interação visual com movimento. Trigger em: "animacao", "transicao", "motion", "micro-interacao", "framer motion", "spring", "easing", "parallax", "scroll animation", "hover effect".
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
O Motion Designer entra após o Frontend, quando os componentes estão prontos para receber vida. Toda animação deve ser intencional, performática e acessível.
Esta skill segue GLOBAL.md, policies/execution.md, policies/handoffs.md, policies/token-efficiency.md, policies/stack-flexibility.md e policies/evals.md.
Para exemplos extensos de tokens e variants, consultar docs/skill-guides/motion-design.md apenas quando necessario.
Para ajustes pontuais de acabamento (scale on press, animacao contextual de icone, skip animation on load, split/stagger de entrada, saida sutil), ver skills/52-ui-polish/SKILL.md — esta skill (12) e dona do sistema de motion tokens; a 52 cobre o detalhe fino que faz o motion parecer refinado.
Animações: Framer Motion
Transições: CSS Transitions / CSS Animations
Estilo: Tailwind CSS
Performance: requestAnimationFrame
Acessibilidade: prefers-reduced-motionexport const motionTokens = {
duration: {
instant: 0.1,
fast: 0.2,
normal: 0.3,
slow: 0.5,
dramatic: 0.8,
},
easing: {
default: [0.25, 0.1, 0.25, 1],
smooth: [0.4, 0, 0.2, 1],
snappy: [0.2, 0, 0, 1],
bounce: [0.34, 1.56, 0.64, 1],
spring: { type: 'spring', stiffness: 300, damping: 20 },
},
stagger: {
fast: 0.03,
normal: 0.05,
slow: 0.1,
},
} as const;import { Variants } from 'framer-motion';
import { motionTokens } from '@/lib/motion-tokens';
const { duration, easing } = motionTokens;
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: duration.normal, ease: easing.smooth },
},
};
export const slideUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: duration.normal, ease: easing.smooth },
},
};
export const slideInFromLeft: Variants = {
hidden: { opacity: 0, x: -30 },
visible: {
opacity: 1,
x: 0,
transition: { duration: duration.normal, ease: easing.snappy },
},
};
export const scaleIn: Variants = {
hidden: { opacity: 0, scale: 0.9 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: duration.fast, ease: easing.bounce },
},
};export const fadeOut: Variants = {
visible: { opacity: 1 },
exit: {
opacity: 0,
transition: { duration: duration.fast, ease: easing.default },
},
};
export const slideDown: Variants = {
visible: { opacity: 1, y: 0 },
exit: {
opacity: 0,
y: 20,
transition: { duration: duration.fast, ease: easing.smooth },
},
};export const staggerContainer: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: motionTokens.stagger.normal,
delayChildren: 0.1,
},
},
};
export const staggerItem: Variants = {
hidden: { opacity: 0, y: 15 },
visible: {
opacity: 1,
y: 0,
transition: { duration: duration.normal, ease: easing.smooth },
},
};import { motion } from 'framer-motion';
import { staggerContainer, staggerItem } from '@/lib/motion-variants';
function AnimatedList({ items }: { items: Item[] }) {
return (
<motion.ul
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{items.slice(0, 10).map((item) => (
<motion.li key={item.id} variants={staggerItem}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}export const pageTransition: Variants = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
transition: { duration: duration.normal, ease: easing.smooth },
},
exit: {
opacity: 0,
y: -10,
transition: { duration: duration.fast, ease: easing.default },
},
};import { AnimatePresence, motion } from 'framer-motion';
import { pageTransition } from '@/lib/motion-variants';
function PageWrapper({ children, key }: { children: React.ReactNode; key: string }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={key}
variants={pageTransition}
initial="hidden"
animate="visible"
exit="exit"
>
{children}
</motion.div>
</AnimatePresence>
);
}import { motion } from 'framer-motion';
import { motionTokens } from '@/lib/motion-tokens';
function ButtonPress({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
return (
<motion.button
whileTap={{ scale: 0.97 }}
whileHover={{ scale: 1.02 }}
transition={{ duration: motionTokens.duration.instant, ease: motionTokens.easing.snappy }}
onClick={onClick}
>
{children}
</motion.button>
);
}
function CardHover({ children }: { children: React.ReactNode }) {
return (
<motion.div
whileHover={{
y: -4,
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.12)',
}}
transition={{ duration: motionTokens.duration.fast, ease: motionTokens.easing.smooth }}
>
{children}
</motion.div>
);
}
function FocusRing({ children }: { children: React.ReactNode }) {
return (
<motion.div
whileFocus={{
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)',
}}
transition={{ duration: motionTokens.duration.instant }}
>
{children}
</motion.div>
);
}import { motion, AnimatePresence } from 'framer-motion';
import { motionTokens } from '@/lib/motion-tokens';
function SkeletonToContent({
isLoading,
skeleton,
children,
}: {
isLoading: boolean;
skeleton: React.ReactNode;
children: React.ReactNode;
}) {
return (
<AnimatePresence mode="wait">
{isLoading ? (
<motion.div
key="skeleton"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: motionTokens.duration.fast }}
>
{skeleton}
</motion.div>
) : (
<motion.div
key="content"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: motionTokens.duration.normal }}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
function SpinnerRotate({ size = 24 }: { size?: number }) {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: motionTokens.duration.dramatic,
repeat: Infinity,
ease: 'linear',
}}
style={{ width: size, height: size }}
className="border-2 border-gray-300 border-t-blue-500 rounded-full"
/>
);
}import { motion } from 'framer-motion';
import { motionTokens } from '@/lib/motion-tokens';
function ScrollFadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{
duration: motionTokens.duration.slow,
ease: motionTokens.easing.smooth,
}}
>
{children}
</motion.div>
);
}Framer Motion whileInView cobre fade/slide-on-scroll simples (ver ScrollFadeIn acima). Para scroll-driven coreografado (pin de seção, scrub vinculado ao progresso do scroll, transições entre cards empilhados), GSAP + ScrollTrigger é a ferramenta certa — Framer Motion não tem pin/scrub nativos com o mesmo controle.
Regra dura: nunca escutar scroll manualmente (window.addEventListener('scroll') em estado React) para dirigir animação — sempre ScrollTrigger (ou useScroll/IntersectionObserver para o caso simples). Listener manual perde sincronia de frame e não limpa corretamente.
import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export function useStickyStack(cardRefs: React.RefObject<HTMLElement>[]) {
useEffect(() => {
const triggers = cardRefs.map((ref, i) => {
if (!ref.current) return null;
return ScrollTrigger.create({
trigger: ref.current,
start: 'top top',
pin: true,
pinSpacing: false,
animation: gsap.to(ref.current, {
scale: 0.92,
opacity: 0.6,
ease: 'none',
}),
endTrigger: cardRefs[i + 1]?.current || undefined,
end: 'top top',
scrub: true,
});
});
return () => triggers.forEach((t) => t?.kill());
}, [cardRefs]);
}export function useHorizontalPan(containerRef: React.RefObject<HTMLElement>, distance: number) {
useEffect(() => {
if (!containerRef.current) return;
const panel = containerRef.current;
const track = panel.querySelector<HTMLElement>('[data-pan-track]');
if (!track) return;
const tween = gsap.to(track, {
x: -distance,
ease: 'none',
scrollTrigger: {
trigger: panel,
start: 'top top',
pin: true,
scrub: 1,
end: `+=${distance}`,
},
});
return () => tween.scrollTrigger?.kill();
}, [containerRef, distance]);
}Sempre limpar (.kill()) no cleanup do useEffect — trigger órfão continua calculando em resize/unmount e vaza memória. Isolar GSAP em Client Components ("use client"), nunca em Server Components.
width, height, top, left, margin, paddingtransform (translate, scale, rotate) e opacitywill-change com moderação e apenas quando necessárioprefers-reduced-motion.animated-element {
will-change: transform, opacity;
}
.animated-element.done {
will-change: auto;
}import { useEffect, useState } from 'react';
export function useReducedMotion(): boolean {
const [prefersReduced, setPrefersReduced] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReduced(mediaQuery.matches);
const handler = (event: MediaQueryListEvent) => {
setPrefersReduced(event.matches);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return prefersReduced;
}import { useReducedMotion } from '@/hooks/useReducedMotion';
function AnimatedComponent({ children }: { children: React.ReactNode }) {
const prefersReduced = useReducedMotion();
return (
<motion.div
initial={prefersReduced ? false : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={
prefersReduced
? { duration: 0 }
: { duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }
}
>
{children}
</motion.div>
);
}A ordem de entrada dos elementos segue a hierarquia visual:
1. Página → Fade/slide da página inteira
2. Seções → Stagger das seções principais
3. Componentes → Entrada individual de cards, listas, etc.
4. Conteúdo → Texto, ícones, badges dentro dos componentesCada nível espera o anterior iniciar antes de começar. Usar delayChildren e staggerChildren para orquestrar a cascata.
const sectionVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: motionTokens.stagger.slow,
delayChildren: motionTokens.duration.fast,
},
},
};
const componentVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth,
staggerChildren: motionTokens.stagger.fast,
},
},
};
const contentVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: motionTokens.duration.fast },
},
};prefers-reduced-motion consideradoMotion NAO cria componentes novos — adiciona animacao e transicoes aos componentes existentes do Frontend. O codigo de motion e adicionado ao MESMO codebase do Frontend. Apos Motion finalizar, o repositorio do Frontend contem os componentes com animacao integrada.
ZERO comentários no código. O código deve ser autoexplicativo através de:
030ef22
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.