make-interfaces-feel-better
Design engineering principles for making interfaces feel polished, with focus on micro-interactions, typography, and visual details.
Install
npx skills add https://github.com/jakubkrehel/make-interfaces-feel-better --skill make-interfaces-feel-betterAnimations
Interruptible animations, enter/exit transitions, contextual icon animations, and motion restraint.
Interruptible Animations
Users change intent mid-interaction. If animations aren't interruptible, the interface feels broken.
CSS Transitions vs. Keyframes
Rule: Always prefer CSS transitions for interactive elements. Reserve keyframes for one-shot sequences.
Enter Animations: Split and Stagger
Use this pattern for infrequent staged entrances where sequence helps communicate hierarchy, such as the first load of a page hero, success state, or empty state. Break a large container into semantic chunks and animate each individually. Do not stagger routine interactions such as row hovers, keystrokes, or repeated tab changes.
Step by Step
- Split into logical groups (title, description, buttons)
- Stagger with ~100ms delay between groups
- For titles, consider splitting into individual words with ~80ms stagger
- Combine
opacity,blur, andtranslateYfor the enter effect
Code Example
CSS-Only Stagger
Exit Animations
Exit animations should be softer and less attention-grabbing than enter animations. The user's focus is moving to the next thing — don't fight for attention.
Subtle Exit (Recommended)
Full Exit (When Context Matters)
Good vs. Bad
Key points:
- Use a small fixed
translateY(e.g.,-12px) instead of the full container height - Keep some directional movement to indicate where the element went
- Exit duration should be shorter than enter duration (150ms vs 300ms)
- Use a subtle exit when it preserves spatial context. Remove immediately when motion adds no information, the interaction repeats frequently, or reduced motion is requested.
Contextual Icon Animations
When icons appear or disappear contextually (on hover, on state change), animate them with opacity, scale, and blur rather than just toggling visibility.
Motion Example
This example uses the motion package. If the project instead has framer-motion, import the same APIs from "framer-motion"; never mix an installed package with the other package's import path.
CSS Transition Approach (No Motion)
If the project doesn't use Motion (Framer Motion), keep both icons in the DOM and cross-fade them with CSS transitions. Because neither icon unmounts, both enter and exit animate smoothly.
The trick: one icon is absolutely positioned on top of the other. Toggling state cross-fades them — the entering icon scales up from 0.25 while the exiting icon scales down to 0.25, both with opacity and blur.
The non-absolute icon (InactiveIcon) defines the layout size. The absolute icon (ActiveIcon) overlays it without affecting flow.
Choosing Between Motion and CSS
Rule: Check the project's package.json. Import from "motion/react" when motion is installed, or from "framer-motion" when framer-motion is installed. If both exist, follow the imports already used by the component or its nearest peers. If neither is present, use the CSS cross-fade pattern — don't add a dependency just for icon transitions.
When to Animate Icons
Important: Always use exactly these values for contextual icon animations — do not deviate:
scale:0.25→1(never use0.5or0.6)opacity:0→1filter:"blur(4px)"→"blur(0px)"transition:{ type: "spring", duration: 0.3, bounce: 0 }— bounce must always be0, never0.1or any other value
Scale on Press
A subtle scale-down on click gives buttons tactile feedback. Always use scale(0.96). Never use a value smaller than 0.95 — anything below feels exaggerated. Use CSS transitions for interruptibility — if the user releases mid-press, it should smoothly return.
Not every button needs this. Add a static prop to your button component that disables the scale effect when the motion would be distracting.
CSS Example
Tailwind Example
Motion Example
Static Prop Pattern
Extract the scale class into a variable and conditionally apply it based on a static prop:
Skip Animation on Page Load
Use initial={false} on AnimatePresence to prevent enter animations from firing on first render. Elements that are already in their default state shouldn't animate in on page load — only on subsequent state changes.
When It Works
Works well for: icon swaps, toggles, tabs, segmented controls — anything that has a default state on page load.
When It Breaks
Don't use initial={false} when the component relies on its initial prop to set up a first-time enter animation, like a staggered page hero or a loading state. In those cases, removing the initial animation skips the entire entrance.
Verify the component still looks right on a full page refresh before applying this.
Motion Restraint
Motion is a budget, not a garnish:
- No custom animation on high-frequency interactions. Repeated interactions get instant feedback or a minimal
opacityorbackground-colortransition at ≤150ms. - Motion is never the only feedback channel. Every animated state change also needs a static cue such as color, icon, or label.
- Brief and precise beats prominent. If a shorter, smaller animation communicates the same thing, use it.
- Honor reduced-motion preferences. Preserve the static cue and remove unnecessary movement.