Why I Wanted AI To Design My Animation System
I like simple animations. Small stuff that makes an interface feel alive, not a Dribbble circus.
On most projects I keep re-writing the same patterns. Fade in. Slide up. Button hover. Micro delays for staggered elements. Every codebase gets a half-baked set of utilities, and I never quite standardise them.
This time I wanted to treat it as a real system. Tokens. Utilities. Predictable classes. All in CSS. No Tailwind. No GSAP. And I wanted to see how far I could push Cursor as an actual pair programmer, not a Stack Overflow proxy.
This post is exactly how I did it. Full prompts. Where Cursor nailed it. Where it hallucinated. And how I steered it back.
Starting Point: A Boring Baseline
Context. I was in a small React + Vite playground, with plain CSS. No framework CSS. No design system. Just a global styles.css and a couple of components.
Before I opened Cursor, I wrote a tiny baseline:
/* styles.css */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #050814;
color: #f8fafc;
}
button {
font: inherit;
}
That was it. Clean slate, no existing animation utilities.
Prompt 1: Ask For The System, Not The Snippet
I opened Cursor, selected styles.css, and started with a broad request. This is the raw prompt:
Prompt 1 (to Cursor):
I want a small, cohesive CSS animation system for a web app.
Constraints:
- Only CSS (no JS-based animation libs)
- No frameworks, just plain CSS
- Designed for a dark, UI-heavy application
Goals:
- Reusable utility classes for: fade, slide, scale, subtle hover
- Animation tokens for durations, timing functions and easing
- Respect “prefers-reduced-motion” without breaking layout
Please:
1. Propose a structure (variables, utility classes, keyframes)
2. Write the CSS
3. Explain how you’d name the classes so they don’t become random garbage
Cursor responded with a pretty solid structure. It proposed:
- CSS variables on
:rootfor durations, delays and easings. - Re-usable utility classes like
.anim-fade-in,.anim-slide-up,.anim-scale-in. - A
@media (prefers-reduced-motion: reduce)block that strips animations.
The generated CSS was roughly like this, reduced to the essentials:
:root {
--anim-duration-fast: 120ms;
--anim-duration-normal: 200ms;
--anim-duration-slow: 320ms;
--anim-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--anim-ease-in: cubic-bezier(0.64, 0, 0.78, 0);
--anim-ease-soft: cubic-bezier(0.25, 0.1, 0.25, 1);
}
.anim-fade-in {
opacity: 0;
animation: fade-in var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes fade-in {
to {
opacity: 1;
}
}
.anim-slide-up {
opacity: 0;
transform: translateY(8px);
animation: slide-up var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes slide-up {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
For a first response, this was good. It gave me a skeleton that matched how I already think about animations. Short, named utilities. Variables on top.
Where it missed: the global reduction of all animation for prefers-reduced-motion is lazy. I care about accessibility, but I do not want to break necessary micro interactions like subtle focus rings.
Prompt 2: Tighten The Reduced Motion Handling
I sent a follow-up. I referenced its own output and pushed it away from the global hammer.
Prompt 2 (to Cursor):
Your prefers-reduced-motion handling is too aggressive.
I don’t want to kill all transitions globally. I want:
- Animation utilities to be no-op when reduced motion is enabled
- Non-animation styles to still work (e.g. color transitions on hover are fine)
Please:
1. Remove the global `* { animation: none }` approach
2. Update the utilities so they respect reduced motion individually
Cursor corrected itself and updated the CSS. The new pattern:
.anim-fade-in {
opacity: 0;
animation: fade-in var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@media (prefers-reduced-motion: reduce) {
.anim-fade-in {
opacity: 1;
animation: none;
}
}
Same for slide, scale, whatever. I liked this more. It keeps the control at the utility level.
Where Cursor still needed guidance: the animation naming and the duplication. Every class got a copy-paste reduced-motion override. That is annoying to maintain.
Prompt 3: Extract A Pattern Instead Of Copy-Paste
I wanted one place to toggle “animation is allowed” without repeating overrides in every utility block.
Here is what I asked:
Prompt 3 (to Cursor):
The reduced-motion overrides are copy-pasted per class.
I want a single source of truth.
Idea:
- A CSS variable (e.g. --anim-enabled) that gets switched to 0 when reduced motion is on
- Utilities should use that for opacity + transform + animation duration
Can you refactor the earlier CSS so that:
- All animation utilities use a shared pattern that can be switched off via variables
- `prefers-reduced-motion` only changes variables, not every class definition
This is where Cursor did something actually useful. It proposed a variable-driven approach:
:root {
--anim-enabled: 1;
--anim-duration-fast: 120ms;
--anim-duration-normal: 200ms;
--anim-duration-slow: 320ms;
}
@media (prefers-reduced-motion: reduce) {
:root {
--anim-enabled: 0;
}
}
.anim-fade-in {
opacity: calc(1 - var(--anim-enabled));
animation-duration: calc(var(--anim-enabled) * var(--anim-duration-normal));
animation-name: fade-in;
animation-timing-function: var(--anim-ease-out);
animation-fill-mode: forwards;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
This was clever in theory and very broken in practice.
Issues:
animation-durationcannot be multiplied by 0 withcalc()in a way that cleanly removes animation. Some browsers get weird with zero-duration animations.opacity: calc(1 - var(--anim-enabled))flips the logic. I do not want elements to be invisible when motion is enabled.- It also makes the CSS harder to read. Magic calc everywhere for a very small benefit.
This is a good example of AI doing the “smart” thing that I would never ship. I like simple explicit CSS more than clever variable gymnastics.
Prompt 4: Back To Basics, With Utilities Only
I pushed it back toward something more boring and maintainable.
Prompt 4 (to Cursor):
The calc-based `--anim-enabled` trick is too clever.
I want something more explicit and readable.
New constraints:
- Each animation utility sets: initial opacity/transform + animation shorthand
- A single `.motion-safe` class opt-in that activates animations
- Under `prefers-reduced-motion: reduce` the `.motion-safe` class should do nothing
So:
- By default, elements render statically
- If I add `.motion-safe anim-fade-in`, it should animate *only* when motion is allowed
Refactor the animation utilities to use this pattern.
This time Cursor produced something I could use almost as-is. The core idea:
.anim-fade-in {
opacity: 0;
}
.motion-safe.anim-fade-in {
animation: fade-in var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes fade-in {
to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.motion-safe.anim-fade-in {
animation: none;
opacity: 1;
}
}
Same pattern for slide and scale. So the DOM looked like:
<div class="motion-safe anim-fade-in">Hello</div>
Now I could choose per element if it should try to animate. Motion preference support stays clean. No global blunt instrument. And you only look in one place to understand what triggers the motion.
This was the first point where I thought “ok, this is actually a useful assistant, not just a code generator”.
Prompt 5: Naming, Or How To Avoid Utility Soup
So far the names were fine, but I know how this goes. Two weeks later you have utilities like .anim-card-enter-soft and nobody remembers what they do.
I wanted Cursor to help me define a minimal vocabulary.
Prompt 5 (to Cursor):
We have:
- .motion-safe (feature flag)
- .anim-fade-in, .anim-slide-up, .anim-scale-in
I want a small, opinionated naming system so this doesn’t collapse into random classes.
Constraints:
- Only a few base motions, not every possible combination
- Variants handled by suffixes (e.g. `-sm`, `-lg` for distance, `-fast` for duration)
- Class names should be guessable without reading the CSS
Propose:
1. A list of core motion types
2. A list of allowed modifiers
3. Updated CSS for one example, e.g. slide-up with sm and lg distance
Cursor suggested the following core types:
anim-fadeanim-slide-up,anim-slide-down,anim-slide-left,anim-slide-rightanim-scale
And modifiers:
- Intensity:
-sm,-md,-lg - Speed:
-fast,-slow
That is basically Tailwind language, but for animations. I am fine with that. At least it is predictable.
The concrete CSS it wrote for slide-up:
.anim-slide-up-sm {
transform: translateY(4px);
opacity: 0;
}
.motion-safe.anim-slide-up-sm {
animation: slide-up-sm var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes slide-up-sm {
to {
transform: translateY(0);
opacity: 1;
}
}
.anim-slide-up-lg {
transform: translateY(16px);
opacity: 0;
}
.motion-safe.anim-slide-up-lg {
animation: slide-up-lg var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes slide-up-lg {
to {
transform: translateY(0);
opacity: 1;
}
}
This is where I cut it off. I do not want separate keyframes for sm and lg. That is pure duplication.
Prompt 6: Reduce Keyframe Duplication
Time to reduce the noise. The important thing is the starting transform, not the end state. We can share keyframes and change only the initial transform.
Prompt 6 (to Cursor):
We don’t need separate keyframes for `slide-up-sm` and `slide-up-lg`.
The end state is the same. Only the starting offset changes.
Refactor:
- Single `@keyframes slide-up` that goes from current transform to none
- Utilities that only set the initial transform and reference the shared keyframe
Apply it to the slide-up example.
Cursor responded with something closer to what I had in mind:
@keyframes slide-up {
to {
transform: translateY(0);
opacity: 1;
}
}
.anim-slide-up-sm {
transform: translateY(4px);
opacity: 0;
}
.motion-safe.anim-slide-up-sm {
animation: slide-up var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
.anim-slide-up-lg {
transform: translateY(16px);
opacity: 0;
}
.motion-safe.anim-slide-up-lg {
animation: slide-up var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
Good. Single keyframe. Different starting points. That is how I would hand-write it.
Prompt 7: Add Hover Micro Interactions
So far this was all entrance animations. I wanted tiny hover behaviours for buttons and cards. Subtle scale, soft shadows. Stuff that users feel more than see.
Prompt 7 (to Cursor):
Now add hover-focused utilities:
- A subtle scale-up for interactive elements
- A softer, longer easing curve
- Designed for a dark UI (no white glowing garbage)
Constraints:
- Should work as composable classes on buttons or cards
- Should not depend on `motion-safe` (hover is fine even with reduced motion)
Give me:
1. New variables if needed
2. 2-3 hover utility classes
Cursor proposed something like:
:root {
--hover-scale-factor: 1.02;
--hover-shadow-soft: 0 18px 45px rgba(15, 23, 42, 0.75);
}
.hover-scale-soft {
transition: transform 160ms var(--anim-ease-soft);
}
.hover-scale-soft:hover {
transform: scale(var(--hover-scale-factor));
}
.hover-elevate {
transition: box-shadow 200ms var(--anim-ease-soft),
transform 200ms var(--anim-ease-soft);
}
.hover-elevate:hover {
transform: translateY(-1px);
box-shadow: var(--hover-shadow-soft);
}
This was one of those times where Cursor basically wrote my muscle memory. I could have typed it faster myself, but it is nice to get a complete, consistent set without thinking too much.
I only tweaked the shadow colour and slightly reduced the scale factor. Anything above 1.02 looks like a cheap landing page.
Where Cursor Needed Real Direction
Across the whole session, a few patterns kept repeating.
1. It Over-Optimises Too Early
The --anim-enabled variable trick is exactly what I mean. Cursor loves clever abstractions. I do not. For small animation systems, clarity beats “one variable to rule them all”.
Fix: I kept asking it to optimise for readability and explicitness, not for pure abstraction.
2. It Duplicates Without Shame
The initial sm / lg keyframes were pure duplication. AI is fine with that. Humans maintain this later.
Fix: Anytime I saw copy-pasted patterns that shared the same end state, I asked Cursor to factor out a shared keyframe or shared base class.
3. Reduced Motion Needs Human Taste
Cursor treated prefers-reduced-motion as a binary kill switch at first. That is the safe thing, but not always the right feeling. Some transitions are part of usability, not flair.
Fix: I made reduced motion an opt-out for entrance animations only, and left small hover transitions alone. That felt like a better compromise. Users who really hate motion have OS-level tools anyway.
What Cursor Got Surprisingly Right
It was not all corrections. A few things it nailed.
1. The Overall System Shape
Variables at the top. Utilities in the middle. Keyframes at the bottom. Motion-safe flag. That layout was immediately workable, even if I replaced pieces.
2. Naming Consistency
The combination of anim-* + -sm / -lg / -fast is not novel, but it is predictable. I care about guessability more than originality here.
3. Not Suggesting JS When I Said CSS
Older AI tools have a bad habit of “helping” with JS-based animation libraries even when you explicitly say you do not want them. Cursor respected the constraint and stayed in CSS land for the entire session.
The Final System I Actually Shipped
I did not keep every line Cursor generated. But the final animations.css looks like this, trimmed down to the core pieces.
:root {
--anim-duration-fast: 120ms;
--anim-duration-normal: 200ms;
--anim-duration-slow: 320ms;
--anim-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--anim-ease-soft: cubic-bezier(0.25, 0.1, 0.25, 1);
--hover-scale-factor: 1.015;
--hover-shadow-soft: 0 18px 45px rgba(15, 23, 42, 0.85);
}
/* Motion-safe flag */
.motion-safe {} /* semantic marker only */
/* Entrance: fade */
.anim-fade-in {
opacity: 0;
}
.motion-safe.anim-fade-in {
animation: fade-in var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes fade-in {
to { opacity: 1; }
}
/* Entrance: slide up variants */
.anim-slide-up-sm {
transform: translateY(4px);
opacity: 0;
}
.anim-slide-up-lg {
transform: translateY(16px);
opacity: 0;
}
.motion-safe.anim-slide-up-sm,
.motion-safe.anim-slide-up-lg {
animation: slide-up var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes slide-up {
to {
transform: translateY(0);
opacity: 1;
}
}
/* Entrance: scale */
.anim-scale-in {
transform: scale(0.98);
opacity: 0;
}
.motion-safe.anim-scale-in {
animation: scale-in var(--anim-duration-normal) var(--anim-ease-out) forwards;
}
@keyframes scale-in {
to {
transform: scale(1);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.motion-safe.anim-fade-in,
.motion-safe.anim-slide-up-sm,
.motion-safe.anim-slide-up-lg,
.motion-safe.anim-scale-in {
animation: none;
opacity: 1;
transform: none;
}
}
/* Hover utilities */
.hover-scale-soft {
transition: transform 140ms var(--anim-ease-soft);
}
.hover-scale-soft:hover {
transform: scale(var(--hover-scale-factor));
}
.hover-elevate {
transition: box-shadow 180ms var(--anim-ease-soft),
transform 180ms var(--anim-ease-soft);
}
.hover-elevate:hover {
transform: translateY(-1px);
box-shadow: var(--hover-shadow-soft);
}
Usage in React stays nice and boring:
<button
className="motion-safe anim-slide-up-sm hover-elevate"
>
Save
</button>
Was Cursor Better Than Stack Overflow?
For this kind of task, yes. Stack Overflow is great when you have a precise error and you want a precise fix. It is not a great tool for shaping a small design system with tradeoffs and taste.
What Cursor gave me was not “the right answer”. It gave me fast, rough drafts that I could argue with. It wrote just enough boilerplate that I could spend my time on the higher-level questions. How explicit should motion flags be. How aggressive should reduced motion be. How much naming structure is enough.
That is the part I actually enjoy. And it is exactly the part where Stack Overflow never really helped anyway.
Subscribe to my newsletter to get the latest updates and news
Member discussion