Interaction Design
Interaction design is the art of making interfaces feel alive and responsive. Every tap, click, hover, and scroll should produce appropriate feedback. When feedback is absent, users feel uncertain. When it's excessive, they feel annoyed. The goal is feedback that's immediate, proportional, and informative.
Feedback Principles
Every user action needs appropriate feedback. The type and intensity of feedback should match the importance of the action.
| User Action | Expected Feedback | Timing | Example |
|---|---|---|---|
| Hover over interactive element | Visual change (color, shadow, cursor) | Immediate (<50ms) | Button darkens slightly |
| Click/tap a button | Immediate visual response | < 100ms | Button press animation + spinner |
| Submit a form | Loading indicator → result | < 100ms to show loading | Button shows spinner, then "Saved" toast |
| Complete a task | Confirmation message | Within 1s of completion | "Order placed! Check your email for confirmation." |
| Error occurs | Clear error explanation | Immediately on detection | Red border on field + inline error message |
| Toggle a switch | State change + position change | < 100ms | Toggle slides, color changes, label updates |
| Delete something | Confirmation → success/undo | Immediate confirmation dialog | "Deleted. [Undo]" toast with 10s timer |
| Drag an element | Visual connection to pointer | Every frame (60fps) | Element follows cursor with slight opacity change |
The Feedback Hierarchy
Not all actions deserve the same level of feedback:
LOW IMPORTANCE HIGH IMPORTANCE
(Subtle feedback) (Prominent feedback)
Hover → Focus → Click → Submit → Error/Success
color outline animation spinner/toast modal/banner
change ring + state + message + persist
change
Rule: The more consequential or irreversible the action, the more prominent the feedback.
Animation Guidelines
When to Animate
| Purpose | Example | Animation Type |
|---|---|---|
| State change | Button hover, toggle switch | Color transition, position shift |
| Feedback | Form submission, button press | Scale, color, spinner |
| Orientation | New content appearing | Fade in, slide from direction of source |
| Attention | New notification, required field | Pulse, shake, bounce |
| Delight | Task completion, milestone | Confetti, checkmark draw, celebration |
| Continuity | Page transitions, expanding cards | Slide, scale, morph |
When NOT to Animate
- When it delays task completion. A 500ms animation before every page load is maddening
- When it adds no information. Gratuitous bouncing logos
- When the user is repeating an action. First expand of an accordion? Animate. Every subsequent expand? Consider faster/no animation.
- When the user prefers reduced motion. Always respect
prefers-reduced-motion
Animation Timing
Duration Guidelines
:root {
/* Micro interactions (hover, focus, small state changes) */
--duration-fast: 100ms;
/* Standard interactions (dropdowns, tooltips, toggles) */
--duration-normal: 200ms;
/* Medium transitions (modals, sidebars, expanding content) */
--duration-medium: 300ms;
/* Large transitions (page transitions, full-screen overlays) */
--duration-slow: 400ms;
/* Complex animations (onboarding, celebrations) */
--duration-complex: 500ms;
}
Rule of thumb: If the animation distance is small (toggle switch), use 100-200ms. If the distance is large (sidebar opening), use 300-400ms. Never exceed 500ms for functional animations.
Easing Functions
Easing makes animations feel natural. Linear motion feels robotic.
| Easing | CSS Value | When to Use |
|---|---|---|
| Ease-out | cubic-bezier(0, 0, 0.2, 1) | Elements entering the screen (fast start, slow end) |
| Ease-in | cubic-bezier(0.4, 0, 1, 1) | Elements leaving the screen (slow start, fast end) |
| Ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | Elements moving between positions |
| Spring | cubic-bezier(0.34, 1.56, 0.64, 1) | Playful, bouncy interactions (toggles, notifications) |
| Linear | linear | Progress bars, color changes, opacity fades |
/* Standard transitions */
.button {
transition: background-color 150ms ease-out;
}
.modal {
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.sidebar {
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
Animation Direction Principles
Animations should reflect the spatial model of the interface:
| Action | Animation Direction |
|---|---|
| Opening a dropdown | Slides/fades down from trigger |
| Closing a dropdown | Fades/slides up toward trigger |
| Opening a modal | Fades in + scales up from center (or slides from bottom on mobile) |
| Closing a modal | Fades out + scales down |
| Navigating forward | Content slides left (new content enters from right) |
| Navigating back | Content slides right (previous content enters from left) |
| Sidebar opening | Slides in from the side it's positioned on |
| Toast notification | Slides in from top-right or bottom-center |
| Deleting an item from a list | Item collapses/fades, siblings animate to fill the gap |
Respecting Motion Preferences
Some users experience motion sickness, migraines, or vestibular disorders from animations. Always respect the system setting:
/* Reduce all animations for users who prefer reduced motion */
@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;
}
}
Better approach: Instead of removing all motion, replace motion-heavy animations with simple fades:
.modal {
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.modal {
transition: opacity 200ms ease-out;
/* Remove transform animation, keep opacity fade */
}
}
Microinteractions
Microinteractions are small, focused animations that provide feedback or delight. They're the difference between an interface that feels mechanical and one that feels alive.
Essential Microinteractions
| Interaction | Implementation | Purpose |
|---|---|---|
| Button press | transform: scale(0.97) on :active | Confirms the tap/click registered |
| Toggle switch | Slide circle + change color, 150ms | Shows state change clearly |
| Checkbox check | Checkmark draws in (SVG animation) | Satisfying confirmation |
| Like/favorite | Heart fills + brief scale-up | Emotional reward, fun |
| Pull to refresh | Spinner appears, content slides down | Indicates refresh in progress |
| Input focus | Border color change + label floats up | Shows which field is active |
| Copy to clipboard | "Copied!" tooltip for 2 seconds | Confirms the action worked |
| Loading dots | Three dots pulsing sequentially | Indicates something is happening |
| Form field validation | Green check fades in on valid | Real-time confirmation |
Implementing a Button Press
.button {
transition: transform 100ms ease-out, background-color 150ms ease-out;
}
.button:hover {
background-color: var(--primary-600);
}
.button:active {
transform: scale(0.97);
background-color: var(--primary-700);
}
/* Subtle shadow lift on hover */
.card {
transition: box-shadow 200ms ease-out, transform 200ms ease-out;
}
.card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
Implementing a Toggle Switch
.toggle {
width: 48px;
height: 28px;
background: #CCC;
border-radius: 14px;
position: relative;
cursor: pointer;
transition: background-color 200ms ease-out;
}
.toggle.is-on {
background: var(--primary-500);
}
.toggle-thumb {
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
/* Spring easing for a bouncy feel */
}
.toggle.is-on .toggle-thumb {
transform: translateX(20px);
}
Loading States
Loading state design directly impacts perceived performance and user patience.
Loading State Decision Tree
| Wait Duration | User Perception | Recommended Feedback |
|---|---|---|
| < 300ms | Instant | No loading indicator needed. Disable the trigger. |
| 300ms - 1s | Brief pause | Subtle indicator: button spinner, content dim |
| 1s - 3s | Noticeable wait | Skeleton screen or spinner |
| 3s - 10s | User actively waiting | Progress bar (determinate if possible) |
| > 10s | Risk of abandonment | Progress bar + time estimate + allow background |
Skeleton Screens
Skeletons show the shape of content before it loads. They're better than spinners because they set expectations about the layout.
Loading with spinner: Loading with skeleton:
┌─────────────────────┐ ┌─────────────────────┐
│ │ │ ░░░░░░░░░░░░░░░░░░ │
│ │ │ │
│ ⟳ Loading... │ │ ░░░░░░░░░░░░░░░░░░░ │
│ │ │ ░░░░░░░░░░░░░░░░░░░ │
│ │ │ ░░░░░░░░░░░░ │
│ │ │ │
│ │ │ ░░░░░░░░ │
└─────────────────────┘ └─────────────────────┘
User has no idea what's coming. User sees the page structure.
/* Skeleton shimmer animation */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Optimistic UI
Show the result immediately, sync with the server in the background. If the server request fails, revert and show an error.
User clicks "Like":
1. Immediately: Heart fills, count increments (no server wait)
2. Background: POST request to server
3. If success: Nothing changes (already updated)
4. If failure: Revert heart, show "Couldn't save. Try again."
Best for: Likes, favorites, toggles, adding items to lists. Low-risk actions where server failure is rare.
Don't use for: Payments, deleting data, sending messages. High-risk actions where failure has consequences.
Scroll-Based Interactions
| Pattern | Description | Use When |
|---|---|---|
| Sticky header | Header stays visible while scrolling | Always on data-heavy pages |
| Scroll-to-top | Button appears after scrolling down | Long pages |
| Infinite scroll | Load more content as user scrolls | Social feeds, search results |
| Scroll-triggered animations | Elements animate when they enter viewport | Marketing/landing pages |
| Parallax | Background moves slower than foreground | Sparingly on marketing pages |
Infinite Scroll vs. Pagination
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| Infinite scroll | Effortless, great for browsing | Can't bookmark position, footer unreachable, uses memory | Social feeds, image galleries |
| Pagination | Bookmarkable, predictable, good for SEO | Requires click, interrupts flow | Search results, products, data tables |
| "Load more" button | User-controlled, less jarring than pagination | Still requires click | Comments, reviews, secondary content |
Gesture Design (Mobile)
| Gesture | Common Action | Example |
|---|---|---|
| Tap | Primary selection | Tap a card to open it |
| Long press | Secondary actions, context menu | Long press to select/edit |
| Swipe horizontal | Navigate between views, or reveal actions | Swipe to delete in email |
| Swipe down | Refresh content or dismiss | Pull to refresh |
| Pinch | Zoom in/out | Map, image zoom |
| Double tap | Quick action or zoom | Double-tap to like (Instagram) |
Important: Every gesture should have a non-gesture alternative (a visible button). Not all users discover gestures, and accessibility tools may not support them.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| No loading indication | Users don't know if their action registered | Show feedback within 100ms of any interaction |
| Animations longer than 400ms | Interface feels sluggish | Keep functional animations under 300ms |
| Not respecting reduced motion | Motion-sensitive users get sick | Implement prefers-reduced-motion media query |
| Skeleton screens that don't match the layout | Sets wrong expectations, causes layout shift | Match skeleton shapes to actual content shapes |
| Spinner for every loading state | Boring, tells user nothing about progress | Use skeletons for content, progress bars for uploads |
| Animating from the wrong direction | Feels unnatural and disorienting | Elements should enter/exit from their logical source |
| No undo for destructive actions | Users can't recover from mistakes | Provide undo toast for 10 seconds, or use soft delete |
| Gesture-only interactions | Some users can't discover or perform gestures | Always provide a visible button alternative |
Key Takeaways
- Every interaction needs feedback. The more important the action, the more prominent the feedback.
- Keep functional animations under 300ms. Use 100-150ms for micro-interactions (hover, press).
- Use ease-out for entering elements, ease-in for exiting, ease-in-out for moving between positions.
- Always implement
prefers-reduced-motion. Replace motion with simple fades. - Prefer skeleton screens over spinners for content loading. They set layout expectations.
- Use optimistic UI for low-risk actions (likes, toggles) to make the interface feel instant.
- Gestures should always have a visible button alternative for accessibility.
- Loading state design: under 300ms = no indicator, 300ms-1s = subtle, 1-3s = skeleton, 3-10s = progress bar.