Interaction Design
Seth HobsonCommunityCreated by Seth Hobson
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a framework for designing and implementing microinteractions, motion design, and state transitions to enhance user experience. It covers core principles of purposeful motion, timing guidelines, and easing functions, while offering practical implementation patterns for loading states, gestures, and feedback systems. The skill emphasizes performance, accessibility, and consistent animation logic within web applications.
What it does
- Defines principles for purposeful motion and user feedback.
- Provides timing and easing guidelines for consistent UI animations.
- Offers implementation patterns for loading states and skeleton screens.
- Demonstrates gesture-based interactions like swipe-to-dismiss.
- Includes accessibility best practices for respecting user motion preferences.
- Provides code patterns for common UI components using animation libraries.
When to use it
- When adding microinteractions to improve user feedback.
- When implementing smooth transitions between pages or components.
- When designing loading states or skeleton screens.
- When creating interactive elements like buttons, toggles, or gesture-based controls.
- When ensuring UI animations are accessible and performant.
Advantages
- Enhances usability through clear visual feedback.
- Improves perceived performance with skeleton screens and smooth transitions.
- Ensures consistent animation behavior across the application.
- Promotes accessibility by respecting user motion settings.
- Provides ready-to-use patterns for common interaction design challenges.
Usage examples
- “Create a button that scales slightly when hovered and tapped.”
- A React component using motion properties to provide tactile feedback on user interaction.
- “Implement a loading state for a card component.”
- A skeleton screen pattern that preserves layout structure while content is being fetched.
- “How do I handle page transitions in my app?”
- A wrapper component using AnimatePresence to animate content entry and exit.
- “Make my toggle switch feel more responsive.”
- A toggle component with spring-based motion for the switch handle and smooth color transitions.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/wshobson/interaction-design/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip interaction-design.zip -d .agents/skills
Documentation by Seth Hobson
Interaction Design
Create engaging, intuitive interactions through motion, feedback, and thoughtful state transitions that enhance usability and delight users.
When to Use This Skill
Adding microinteractions to enhance user feedback
Implementing smooth page and component transitions
Designing loading states and skeleton screens
Creating gesture-based interactions
Building notification and toast systems
Implementing drag-and-drop interfaces
Adding scroll-triggered animations
Designing hover and focus states
Core Principles
1. Purposeful Motion
Motion should communicate, not decorate:
Feedback: Confirm user actions occurred
Orientation: Show where elements come from/go to
Focus: Direct attention to important changes
Continuity: Maintain context during transitions
2. Timing Guidelines
| Duration | Use Case |
|---|---|
| 100-150ms | Micro-feedback (hovers, clicks) |
| 200-300ms | Small transitions (toggles, dropdowns) |
| 300-500ms | Medium transitions (modals, page changes) |
| 500ms+ | Complex choreographed animations |
3. Easing Functions
/* Common easings */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Decelerate - entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accelerate - exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Both - moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Overshoot - playful */Quick Start: Button Microinteraction
import { motion } from "framer-motion";
export function InteractiveButton({ children, onClick }) {
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
{children}
</motion.button>
);
}Interaction Patterns
1. Loading States
Skeleton Screens: Preserve layout while loading
function CardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-48 bg-gray-200 rounded-lg" />
<div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
);
}Progress Indicators: Show determinate progress
function ProgressBar({ progress }: { progress: number }) {
return (
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ ease: "easeOut" }}
/>
</div>
);
}2. State Transitions
Toggle with smooth transition:
function Toggle({ checked, onChange }) {
return (
<button
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`
relative w-12 h-6 rounded-full transition-colors duration-200
${checked ? "bg-blue-600" : "bg-gray-300"}
`}
>
<motion.span
className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
animate={{ x: checked ? 24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
);
}3. Page Transitions
Framer Motion layout animations:
import { AnimatePresence, motion } from "framer-motion";
function PageTransition({ children, key }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={key}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}4. Feedback Patterns
Ripple effect on click:
function RippleButton({ children, onClick }) {
const [ripples, setRipples] = useState([]);
const handleClick = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const ripple = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
id: Date.now(),
};
setRipples((prev) => [...prev, ripple]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== ripple.id));
}, 600);
onClick?.(e);
};
return (
<button onClick={handleClick} className="relative overflow-hidden">
{children}
{ripples.map((ripple) => (
<span
key={ripple.id}
className="absolute bg-white/30 rounded-full animate-ripple"
style={{ left: ripple.x, top: ripple.y }}
/>
))}
</button>
);
}5. Gesture Interactions
Swipe to dismiss:
function SwipeCard({ children, onDismiss }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss();
}
}}
className="cursor-grab active:cursor-grabbing"
>
{children}
</motion.div>
);
}CSS Animation Patterns
Keyframe Animations
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}CSS Transitions
.card {
transition:
transform 0.2s ease-out,
box-shadow 0.2s ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}Accessibility Considerations
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}function AnimatedComponent() {
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
return (
<motion.div
animate={{ opacity: 1 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
/>
);
}Best Practices
Performance First: Use
transformandopacityfor smooth 60fpsReduce Motion Support: Always respect
prefers-reduced-motionConsistent Timing: Use a timing scale across the app
Natural Physics: Prefer spring animations over linear
Interruptible: Allow users to cancel long animations
Progressive Enhancement: Work without JS animations
Test on Devices: Performance varies significantly
Common Issues
Janky Animations: Avoid animating
width,height,top,leftOver-animation: Too much motion causes fatigue
Blocking Interactions: Never prevent user input during animations
Memory Leaks: Clean up animation listeners on unmount
Flash of Content: Use
will-changesparingly for optimization
Files
- ATTRIBUTION.mdAdded by AutoGineer972 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md7.4 KiB
- references/animation-libraries.md10.7 KiB
- references/microinteraction-patterns.md13.2 KiB
- references/scroll-animations.md9.1 KiB
references/scroll-animations.md
Scroll Animations Reference
Intersection Observer Hook
import { useEffect, useRef, useState, type RefObject } from "react";
interface UseInViewOptions {
threshold?: number | number[];
rootMargin?: string;
triggerOnce?: boolean;
}
function useInView<T extends HTMLElement>({
threshold = 0,
rootMargin = "0px",
triggerOnce = false,
}: UseInViewOptions = {}): [RefObject<T>, boolean] {
const ref = useRef<T>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
const inView = entry.isIntersecting;
setIsInView(inView);
if (inView && triggerOnce) {
observer.unobserve(element);
}
},
{ threshold, rootMargin },
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return [ref, isInView];
}
// Usage
function FadeInSection({ children }) {
const [ref, isInView] = useInView({ threshold: 0.2, triggerOnce: true });
return (
<div
ref={ref}
className={`transition-all duration-700 ${
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
{children}
</div>
);
}Scroll Progress Indicator
import { motion, useScroll, useSpring } from "framer-motion";
function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left z-50"
style={{ scaleX }}
/>
);
}Parallax Scrolling
Simple CSS Parallax
.parallax-container {
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
perspective: 10px;
}
.parallax-layer-back {
transform: translateZ(-10px) scale(2);
}
.parallax-layer-base {
transform: translateZ(0);
}Framer Motion Parallax
import { motion, useScroll, useTransform } from "framer-motion";
function ParallaxHero() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], ["0%", "50%"]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 1.2]);
return (
<section ref={ref} className="relative h-screen overflow-hidden">
{/* Background image with parallax */}
<motion.div style={{ y, scale }} className="absolute inset-0">
<img src="/hero-bg.jpg" alt="" className="w-full h-full object-cover" />
</motion.div>
{/* Content fades out on scroll */}
<motion.div
style={{ opacity }}
className="relative z-10 flex items-center justify-center h-full"
>
<h1 className="text-6xl font-bold text-white">Welcome</h1>
</motion.div>
</section>
);
}Scroll-Linked Animations
Progress-Based Animation
function ScrollAnimation() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"],
});
// Different transformations based on scroll progress
const x = useTransform(scrollYProgress, [0, 1], [-200, 200]);
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);
const backgroundColor = useTransform(
scrollYProgress,
[0, 0.5, 1],
["#3b82f6", "#8b5cf6", "#ec4899"],
);
return (
<div ref={containerRef} className="h-[200vh] py-20">
<div className="sticky top-1/2 -translate-y-1/2 flex justify-center">
<motion.div
style={{ x, rotate, backgroundColor }}
className="w-32 h-32 rounded-2xl"
/>
</div>
</div>
);
}Horizontal Scroll Section
function HorizontalScroll({ items }) {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"],
});
const x = useTransform(
scrollYProgress,
[0, 1],
["0%", `-${(items.length - 1) * 100}%`],
);
return (
<section ref={containerRef} className="relative h-[300vh]">
<div className="sticky top-0 h-screen overflow-hidden">
<motion.div style={{ x }} className="flex h-full">
{items.map((item, i) => (
<div
key={i}
className="flex-shrink-0 w-screen h-full flex items-center justify-center"
>
{item}
</div>
))}
</motion.div>
</div>
</section>
);
}Reveal Animations
Staggered List Reveal
function StaggeredList({ items }) {
const [ref, isInView] = useInView({ threshold: 0.1, triggerOnce: true });
return (
<ul ref={ref} className="space-y-4">
{items.map((item, i) => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ delay: i * 0.1, duration: 0.5 }}
className="p-4 bg-white rounded-lg shadow"
>
{item.content}
</motion.li>
))}
</ul>
);
}Text Reveal
function TextReveal({ text }) {
const [ref, isInView] = useInView({ threshold: 0.5, triggerOnce: true });
const words = text.split(" ");
return (
<p ref={ref} className="text-4xl font-bold">
{words.map((word, i) => (
<motion.span
key={i}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: i * 0.05, duration: 0.3 }}
className="inline-block mr-2"
>
{word}
</motion.span>
))}
</p>
);
}Clip Path Reveal
function ClipReveal({ children }) {
const [ref, isInView] = useInView({ threshold: 0.3, triggerOnce: true });
return (
<motion.div
ref={ref}
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={isInView ? { clipPath: "inset(0 0% 0 0)" } : {}}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
>
{children}
</motion.div>
);
}Sticky Scroll Sections
function StickySection({ title, content, image }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 0.8]);
return (
<section ref={ref} className="relative h-[200vh]">
<motion.div
style={{ opacity, scale }}
className="sticky top-0 h-screen flex items-center"
>
<div className="grid grid-cols-2 gap-16 container mx-auto">
<div>
<h2 className="text-4xl font-bold">{title}</h2>
<p className="mt-4 text-lg text-gray-600">{content}</p>
</div>
<div>
<img src={image} alt="" className="rounded-2xl shadow-xl" />
</div>
</div>
</motion.div>
</section>
);
}Smooth Scroll
// Using CSS
html {
scroll-behavior: smooth;
}
// Using Lenis for butter-smooth scrolling
import Lenis from '@studio-freight/lenis';
function SmoothScrollProvider({ children }) {
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
direction: 'vertical',
smooth: true,
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
return () => lenis.destroy();
}, []);
return children;
}Scroll Snap
/* Scroll snap container */
.snap-container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
height: 100vh;
}
.snap-section {
scroll-snap-align: start;
height: 100vh;
}
/* Smooth scrolling with snap */
@supports (scroll-snap-type: y mandatory) {
.snap-container {
scroll-behavior: smooth;
}
}function FullPageScroll({ sections }) {
return (
<div className="snap-container">
{sections.map((section, i) => (
<section
key={i}
className="snap-section flex items-center justify-center"
>
{section}
</section>
))}
</div>
);
}Performance Optimization
// Use will-change sparingly
const AnimatedElement = styled(motion.div)`
will-change: transform;
`;
// Debounce scroll handlers
function useThrottledScroll(callback, delay = 16) {
const lastRun = useRef(0);
useEffect(() => {
const handler = () => {
const now = Date.now();
if (now - lastRun.current >= delay) {
lastRun.current = now;
callback();
}
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, [callback, delay]);
}
// Use transform instead of top/left
// Good
const goodAnimation = { transform: "translateY(100px)" };
// Bad (causes reflow)
const badAnimation = { top: "100px" };Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Seth Hobson
- Publisher
- Seth Hobson (Community)
- Source
- wshobson/agents · plugins/ui-design/skills/interaction-design · commit 4bde316
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Seth Hobson.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download interaction-design.zip