npx claudepluginhub gabelul/stitch-kit --plugin stitch-kitThis skill is limited to using the following tools:
You are a motion design engineer. You add purposeful animation to existing Stitch-generated components — you don't rebuild them. Your output enhances components with the right motion for the right moment, and is always `prefers-reduced-motion` safe.
Provides Framer Motion and CSS code for web animations including page transitions, component enter/exit, scroll effects, hover interactions, and modals.
Analyzes Next.js + Tailwind + React projects to implement purposeful UI animations like hero intros, hovers, scroll reveals, and page transitions for better UX.
Adds animation polish to apps: button press feedback, hover states, modal/dropdown transitions, loading skeletons, toasts, staggered lists. Uses CSS/Tailwind respecting prefers-reduced-motion.
Share bugs, ideas, or general feedback.
You are a motion design engineer. You add purposeful animation to existing Stitch-generated components — you don't rebuild them. Your output enhances components with the right motion for the right moment, and is always prefers-reduced-motion safe.
Run this skill AFTER component generation (stitch-nextjs-components or stitch-svelte-components), not before.
Use this skill when:
Analyze the design first. Assign animations by tier — don't animate everything:
| Tier | What | Duration | Easing | Examples |
|---|---|---|---|---|
| Micro | Hover, focus, active states on interactive elements | 100–200ms | ease-out | Button hover, link color, icon scale |
| Meso | UI elements entering or leaving the viewport | 250–400ms | cubic-bezier(0,0,0.2,1) | Card reveals, sidebar slide, modal open |
| Macro | Full page or section transitions | 400–600ms | ease-in-out | Route transitions, hero section, onboarding |
Rule of thumb: If in doubt, use Micro. Over-animation is worse than no animation.
Read the generated component files. For each one, identify:
onClick)Only animate elements that have clear purpose. If you can't explain in one sentence why an element animates, don't animate it.
Read package.json to determine the framework, then use the matching approach:
| Framework | Approach |
|---|---|
| Next.js / React | CSS + optionally Framer Motion |
| SvelteKit / Svelte | Built-in Svelte transitions + CSS |
| Vanilla HTML | CSS only |
Use CSS for Micro tier and simple Meso. Zero dependencies.
Add these to design-tokens.css or the component's CSS:
/* Base transition shorthand — use on all interactive elements */
.transition-base {
transition:
background-color var(--motion-duration-fast) var(--motion-ease-default),
color var(--motion-duration-fast) var(--motion-ease-default),
border-color var(--motion-duration-fast) var(--motion-ease-default),
box-shadow var(--motion-duration-fast) var(--motion-ease-default),
transform var(--motion-duration-fast) var(--motion-ease-default),
opacity var(--motion-duration-fast) var(--motion-ease-default);
}
/* Button micro-interaction */
.btn {
transition: transform 150ms ease-out, box-shadow 150ms ease-out, background-color 150ms ease-out;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
/* Card lift */
.card {
transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
Use keyframe animations with animation-fill-mode: both:
@keyframes fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(24px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-up { animation: fade-up var(--motion-duration-base) var(--motion-ease-out) both; }
.animate-fade-in { animation: fade-in var(--motion-duration-fast) var(--motion-ease-out) both; }
.animate-slide-in-r { animation: slide-in-right var(--motion-duration-base) var(--motion-ease-out) both; }
/* Stagger children with CSS custom property */
.stagger-children > * {
animation-delay: calc(var(--stagger-index, 0) * 60ms);
}
Always add this override at the end of every animation CSS block:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Use Framer Motion for Meso and Macro tier in React projects. It handles prefers-reduced-motion natively via useReducedMotion.
npm install framer-motion
'use client'
import { motion, useReducedMotion } from 'framer-motion'
/**
* Wraps children in a scroll-triggered fade+rise animation.
* Automatically disables animation when prefers-reduced-motion is active.
*/
export function RevealOnScroll({ children, delay = 0 }: {
children: React.ReactNode
delay?: number
}) {
const shouldReduce = useReducedMotion()
return (
<motion.div
initial={shouldReduce ? false : { opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{
duration: 0.4,
ease: [0, 0, 0.2, 1],
delay,
}}
>
{children}
</motion.div>
)
}
'use client'
import { motion, useReducedMotion } from 'framer-motion'
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.08 }
}
}
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0, transition: { ease: [0, 0, 0.2, 1], duration: 0.35 } }
}
export function AnimatedGrid({ cards }: { cards: CardProps[] }) {
const shouldReduce = useReducedMotion()
if (shouldReduce) {
return <div className="grid">{cards.map(c => <Card key={c.id} {...c} />)}</div>
}
return (
<motion.div className="grid" variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
{cards.map(c => (
<motion.div key={c.id} variants={item}>
<Card {...c} />
</motion.div>
))}
</motion.div>
)
}
// app/template.tsx — wraps every page with a transition
'use client'
import { motion } from 'framer-motion'
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
)
}
Svelte's built-in transitions are the cleanest option for Svelte projects — zero dependencies.
Svelte doesn't have a built-in scroll reveal, but the use: directive makes this clean:
<script lang="ts">
import { fade, fly } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
/**
* Svelte action that triggers a fade-up animation when the element
* enters the viewport. Respects prefers-reduced-motion.
*/
function revealOnScroll(node: HTMLElement) {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (prefersReduced) return {}
node.style.opacity = '0'
node.style.transform = 'translateY(16px)'
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
node.style.transition = `opacity 400ms cubic-bezier(0,0,0.2,1), transform 400ms cubic-bezier(0,0,0.2,1)`
node.style.opacity = '1'
node.style.transform = 'translateY(0)'
observer.unobserve(node)
}
},
{ threshold: 0.1, rootMargin: '-50px' }
)
observer.observe(node)
return {
destroy() { observer.disconnect() }
}
}
</script>
<!-- Use on any element -->
<section use:revealOnScroll>
<h2>This section fades in on scroll</h2>
</section>
<script lang="ts">
import { fly } from 'svelte/transition'
import { quintOut } from 'svelte/easing'
let items = $state<Item[]>([...])
</script>
{#each items as item, i (item.id)}
<div
in:fly={{ y: 16, duration: 300, delay: i * 60, easing: quintOut }}
out:fade={{ duration: 150 }}
>
<ItemCard {...item} />
</div>
{/each}
<script lang="ts">
import { fade, fly } from 'svelte/transition'
let { isOpen = false } = $props()
</script>
{#if isOpen}
<!-- Backdrop -->
<div
class="backdrop"
transition:fade={{ duration: 200 }}
role="presentation"
/>
<!-- Drawer -->
<aside
class="drawer"
transition:fly={{ x: 320, duration: 300, easing: cubicOut }}
role="dialog"
aria-modal="true"
>
{@render children()}
</aside>
{/if}
When modifying existing files:
scroll-behavior: smooth where appropriate, and even that needs the reduced-motion override| Issue | Fix |
|---|---|
| Animation not playing | Check the element is in the DOM before the animation fires |
| Framer Motion hydration error | Ensure component has 'use client' directive |
| Svelte transition plays twice | Check for double-render in dev mode (StrictMode equivalent) |
| Animation jank/lag | Add will-change: transform, opacity sparingly to animated elements |
| Reduced motion not stopping animation | Ensure @media (prefers-reduced-motion) is loaded AFTER animation CSS |
resources/animation-patterns.md — Catalog of copy-paste ready patterns for common UI components