Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
# Animation & Microinteractions
|
||||
|
||||
Guidelines for when and how to animate UI elements effectively.
|
||||
|
||||
## The Purpose of Animation
|
||||
|
||||
Animation should serve a function, not just look nice.
|
||||
|
||||
### Valid Reasons to Animate
|
||||
|
||||
| Purpose | Example |
|
||||
|---------|---------|
|
||||
| **Feedback** | Button press confirmation, form submission success |
|
||||
| **Orientation** | Showing where something came from or went to |
|
||||
| **Focus** | Drawing attention to important changes |
|
||||
| **Teaching** | Demonstrating how something works |
|
||||
| **Continuity** | Maintaining context during state changes |
|
||||
| **Delight** | Occasional surprise (use sparingly) |
|
||||
|
||||
### Invalid Reasons to Animate
|
||||
|
||||
- "It looks cool"
|
||||
- "The competition does it"
|
||||
- "To show off our skills"
|
||||
- Every state change
|
||||
- To hide slow performance
|
||||
|
||||
---
|
||||
|
||||
## Timing & Duration
|
||||
|
||||
### Duration Guidelines
|
||||
|
||||
| Animation Type | Duration | Rationale |
|
||||
|----------------|----------|-----------|
|
||||
| Micro-feedback (hover, press) | 100-150ms | Must feel instant |
|
||||
| Simple transitions (fade, slide) | 150-250ms | Noticeable but quick |
|
||||
| Complex transitions (modal, navigation) | 250-350ms | Need time to follow |
|
||||
| Entrances/Reveals | 200-400ms | Can be slightly longer |
|
||||
| Decorative/Emphasis | 300-500ms | Purpose is to be noticed |
|
||||
|
||||
### The 200ms Rule
|
||||
|
||||
Most UI animations should be around 200ms:
|
||||
- Faster than 100ms → Too fast to perceive
|
||||
- Slower than 400ms → Feels sluggish, interrupts flow
|
||||
|
||||
**Exception:** Loading and progress indicators can be slower because they represent real waiting.
|
||||
|
||||
### Duration by Distance
|
||||
|
||||
Longer travel distance = longer duration (but not proportionally).
|
||||
|
||||
```
|
||||
Small movement (8-16px): 100-150ms
|
||||
Medium movement (50-100px): 150-250ms
|
||||
Large movement (full screen): 250-350ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Easing Functions
|
||||
|
||||
Easing makes motion feel natural. Linear motion looks robotic.
|
||||
|
||||
### Common Easing Curves
|
||||
|
||||
| Easing | Use For | Feel |
|
||||
|--------|---------|------|
|
||||
| **ease-out** | Elements entering | Fast start, gentle stop |
|
||||
| **ease-in** | Elements leaving | Gentle start, fast exit |
|
||||
| **ease-in-out** | Elements moving within view | Smooth throughout |
|
||||
| **linear** | Progress indicators, opacity changes | Mechanical (intentional) |
|
||||
|
||||
### When to Use Each
|
||||
|
||||
**Ease-out (default for entrances):**
|
||||
```css
|
||||
transition: transform 200ms ease-out;
|
||||
```
|
||||
- Modals appearing
|
||||
- Notifications sliding in
|
||||
- Dropdowns opening
|
||||
- Tooltips appearing
|
||||
|
||||
**Ease-in (for exits):**
|
||||
```css
|
||||
transition: opacity 150ms ease-in;
|
||||
```
|
||||
- Modals dismissing
|
||||
- Elements fading out
|
||||
- Notifications leaving
|
||||
|
||||
**Ease-in-out (for on-screen movement):**
|
||||
```css
|
||||
transition: transform 250ms ease-in-out;
|
||||
```
|
||||
- Tab indicators sliding
|
||||
- Carousel transitions
|
||||
- Drawer/sidebar toggling
|
||||
|
||||
### Custom Cubic Bezier
|
||||
|
||||
For more personality, customize curves:
|
||||
|
||||
```css
|
||||
/* Snappy entrance */
|
||||
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
/* Smooth overshoot */
|
||||
transition: transform 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Animation Patterns
|
||||
|
||||
### Button States
|
||||
|
||||
**Hover:**
|
||||
```css
|
||||
.btn {
|
||||
transition: background-color 100ms ease-out, transform 100ms ease-out;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
```
|
||||
|
||||
**Active/Pressed:**
|
||||
```css
|
||||
.btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
```
|
||||
|
||||
**Loading state:**
|
||||
```css
|
||||
.btn.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
.btn.loading .spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
```
|
||||
|
||||
### Modal Entrance/Exit
|
||||
|
||||
**Enter:**
|
||||
```css
|
||||
.modal {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
transition: opacity 200ms ease-out, transform 200ms ease-out;
|
||||
}
|
||||
.modal.open {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
```
|
||||
|
||||
**Exit:**
|
||||
```css
|
||||
.modal.closing {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transition: opacity 150ms ease-in, transform 150ms ease-in;
|
||||
}
|
||||
```
|
||||
|
||||
### Dropdown/Menu
|
||||
|
||||
```css
|
||||
.dropdown {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms ease-out, transform 150ms ease-out;
|
||||
}
|
||||
.dropdown.open {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Toast/Notification
|
||||
|
||||
```css
|
||||
.toast {
|
||||
transform: translateX(100%);
|
||||
transition: transform 300ms ease-out;
|
||||
}
|
||||
.toast.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.toast.exiting {
|
||||
transform: translateX(100%);
|
||||
transition: transform 200ms ease-in;
|
||||
}
|
||||
```
|
||||
|
||||
### Skeleton Loading
|
||||
|
||||
```css
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#f0f0f0 25%,
|
||||
#e0e0e0 50%,
|
||||
#f0f0f0 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading States
|
||||
|
||||
### Types of Loading Indicators
|
||||
|
||||
| Type | Use When | Example |
|
||||
|------|----------|---------|
|
||||
| **Spinner** | Unknown duration, short wait expected | Button submission |
|
||||
| **Progress bar** | Known progress, longer operations | File upload |
|
||||
| **Skeleton** | Loading content layout | Feed items |
|
||||
| **Pulse/Shimmer** | Refreshing existing content | Pull to refresh |
|
||||
|
||||
### Spinner Guidelines
|
||||
|
||||
- Don't show immediately (wait 300-500ms)
|
||||
- If wait < 1 second, spinner may not be needed
|
||||
- Position in context (where content will appear)
|
||||
- Provide cancel option for long operations
|
||||
|
||||
```jsx
|
||||
// Delay spinner to avoid flash for fast operations
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
const timer = setTimeout(() => setShowSpinner(true), 400);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
setShowSpinner(false);
|
||||
}, [isLoading]);
|
||||
```
|
||||
|
||||
### Progress Bar Guidelines
|
||||
|
||||
- Show percentage when meaningful
|
||||
- Don't let it jump backwards
|
||||
- Consider indeterminate state if progress unknown
|
||||
- Complete to 100% before hiding
|
||||
|
||||
### Skeleton Screen Guidelines
|
||||
|
||||
- Match layout of actual content
|
||||
- Use consistent bone shapes
|
||||
- Animate subtly (shimmer, not bounce)
|
||||
- Replace with content immediately when loaded
|
||||
|
||||
---
|
||||
|
||||
## Microinteractions
|
||||
|
||||
Small animations that provide feedback and delight.
|
||||
|
||||
### Effective Microinteractions
|
||||
|
||||
**Toggle switches:**
|
||||
```css
|
||||
.toggle-thumb {
|
||||
transition: transform 150ms ease-out;
|
||||
}
|
||||
.toggle.on .toggle-thumb {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
```
|
||||
|
||||
**Checkbox check:**
|
||||
```css
|
||||
.checkmark {
|
||||
stroke-dasharray: 20;
|
||||
stroke-dashoffset: 20;
|
||||
transition: stroke-dashoffset 200ms ease-out;
|
||||
}
|
||||
.checkbox.checked .checkmark {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Like/Heart animation:**
|
||||
```css
|
||||
.heart {
|
||||
transform: scale(1);
|
||||
transition: transform 150ms ease-out;
|
||||
}
|
||||
.heart.liked {
|
||||
animation: pop 300ms ease-out;
|
||||
}
|
||||
@keyframes pop {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
```
|
||||
|
||||
**Input focus:**
|
||||
```css
|
||||
.input {
|
||||
border-color: #ccc;
|
||||
transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### When Microinteractions Help
|
||||
|
||||
- Confirming user action occurred
|
||||
- Showing state change clearly
|
||||
- Making interface feel responsive
|
||||
- Guiding attention to changes
|
||||
|
||||
### When to Skip
|
||||
|
||||
- Repetitive actions (every keystroke)
|
||||
- Performance-critical paths
|
||||
- Accessibility mode (respect reduce-motion)
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Considerations
|
||||
|
||||
### Respect User Preferences
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provide Alternatives
|
||||
|
||||
- Don't rely on animation alone to convey information
|
||||
- Ensure state changes are visible without animation
|
||||
- Allow users to disable animations in app settings
|
||||
|
||||
### Avoid Problematic Animations
|
||||
|
||||
| Avoid | Reason |
|
||||
|-------|--------|
|
||||
| Flashing/strobing | Can trigger seizures |
|
||||
| Parallax scrolling | Causes motion sickness |
|
||||
| Auto-playing video | Distracting, accessibility |
|
||||
| Infinite loops | Drains attention, battery |
|
||||
|
||||
---
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
### GPU-Accelerated Properties
|
||||
|
||||
Animate these for smooth 60fps:
|
||||
- `transform` (translate, scale, rotate)
|
||||
- `opacity`
|
||||
|
||||
Avoid animating (causes reflow/repaint):
|
||||
- `width`, `height`
|
||||
- `top`, `left`, `right`, `bottom`
|
||||
- `margin`, `padding`
|
||||
- `border-width`
|
||||
- `font-size`
|
||||
|
||||
### Use will-change Sparingly
|
||||
|
||||
```css
|
||||
/* Only for elements about to animate */
|
||||
.modal {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Remove after animation */
|
||||
.modal.static {
|
||||
will-change: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Animations
|
||||
|
||||
Start animations together, not staggered excessively:
|
||||
- 0-50ms stagger: feels cohesive
|
||||
- 100ms+ stagger: feels slow, sequential
|
||||
|
||||
### Test on Low-end Devices
|
||||
|
||||
What's smooth on your MacBook may stutter on a budget Android phone. Test on real devices or throttle CPU in DevTools.
|
||||
|
||||
---
|
||||
|
||||
## Animation Checklist
|
||||
|
||||
Before shipping an animation:
|
||||
|
||||
- [ ] Does it serve a purpose (not just decoration)?
|
||||
- [ ] Is duration appropriate for the action?
|
||||
- [ ] Does easing feel natural?
|
||||
- [ ] Does it work with `prefers-reduced-motion`?
|
||||
- [ ] Is it GPU-accelerated (transform/opacity)?
|
||||
- [ ] Does it perform well on low-end devices?
|
||||
- [ ] Can the interface function without it?
|
||||
Reference in New Issue
Block a user