Atomic CSS-Based Icon Animation Solution
5/31/2026
I. Background
A redesign of an enterprise-grade backend system's sidebar required adding micro-interaction animations to sidebar icons. The system has approximately 200 business modules, with over 2,000 icons in the sidebar.
As you can imagine, editing animations one by one using traditional After Effects tools would be an enormous amount of work.
Therefore, a solution was needed to quickly generate icon animations while maintaining consistency across all animations.
II. Technology Evaluation
Before starting, we evaluated the mainstream icon animation solutions available:
| Solution | Pros | Cons |
|---|---|---|
| GIF | Good compatibility, WYSIWYG | Large file size, fixed resolution, difficult dark mode adaptation, no interactive control |
| Lottie | Rich animations, supports complex paths | Requires JSON file + lottie-web (~60KB), separate from CSS ecosystem, rendering overhead |
| CSS | Vector lossless, small footprint, no JS runtime, integrates with Tailwind | Limited expressive power for complex animations |
Console icon animations mainly consist of translation, rotation, scaling, stroke drawing, and opacity changes — most of which CSS can handle. However, using Lottie for 2,000 icons would mean attaching a JSON file + runtime to each, resulting in unacceptable bundle size; using GIF would make dark mode impossible to handle. Neither of these two options was viable from the start.
Conclusion: implement animations using a CSS-based approach.
III. Atomic CSS Animations
3.1 Atomization
After deciding on the underlying technology, we still needed to figure out how to implement icon animations.
The "scale" problem remained: with 2,000 icons, writing keyframes and animations one by one would be no different from the original workload. So we needed a CSS-based solution for quickly creating animations.
Tailwind CSS's atomic approach inspired me: break animations down into the smallest units, and let designers assemble them. Just as Tailwind uses flex, p-4, and text-sm to build UIs, animations should be composable using scale-75, rotate-12, and similar utilities.
Moreover, AI excels at selecting and combining from a predefined set of parts — it's much more stable than writing keyframes from scratch.
Following this direction, I discovered tailwindcss-motion, an atomic animation library for Tailwind v3.
The animation taxonomy and preset values in tailwindcss-motion provided a solid starting point, saving the effort of defining everything from scratch. However, it was built on Tailwind v3's JS plugin architecture (matchUtilities + addBase), which couldn't be used directly in Tailwind v4's pure CSS-based system. I used Claude Code to rewrite the entire library into Tailwind v4's @utility + CSS Variable implementation, while trimming and extending it for SVG icon animation scenarios:
Additions:
-
Stroke (draw): An animation type not present in tailwindcss-motion. Achieves line drawing/growth via
stroke-dashoffset, requiring SVG elements to havepathLength="1". Three slots were added:draw-in,draw-out, anddraw-loop.A small trick in the stroke animation implementation:
stroke-dasharrayisn't set exactly to1, but to1.1, with an initial offset of0.05forstroke-dashoffset. The reason is that whendasharrayexactly equals the path length, a slight flicker occurs at the end of the animation due to precision issues (browsers have unstable subpixel rendering near boundary values). By slightly exceeding the path length and offsetting the starting point, the dash never touches that critical boundary, and the flicker disappears. -
Shake: Built-in multi-level decaying oscillation in keyframes (amplitude decays stepwise: -0.5, 0.25, -0.1, 0), reusing the rotate slot without adding new CSS variables.
-
Reverse animations: Supports negative prefix syntax, e.g.,
-motion-translate-x-in-50(reverse translation),-motion-draw-in-100(draw from path end in reverse). More intuitive for both AI generation and manual writing. -
Trigger mechanism: tailwindcss-motion's animations play automatically on element mount. This solution adds a
data-motion-triggerattribute system supporting hover and click triggers. When not triggered,animation-name: none !importantcompletely suppresses the animation, with no reliance on JS events. -
SVG transform-origin fix: SVG elements default to
transform-origin: 0 0(top-left), while HTML elements default to50% 50%(center). It automatically setstransform-box: fill-boxwithin the[data-motion-icon]scope, ensuring rotation and scaling are centered around the element itself.
Removals:
- Preset system: tailwindcss-motion has 30+ pre-composed animation presets (
motion-preset-fade,motion-preset-slide, etc.). In this solution, this responsibility is handed to AI — the AI composes solutions from atomic parts based on icon semantics, rather than humans selecting from presets. - Filter, text-color, and background-color animations: Not needed in icon animation scenarios, removed directly to reduce slot count and CSS size.
- Loop default changed from
infiniteto1: Admin dashboard icon looping animations usually only need a finite number of repetitions (e.g., a gear rotating one or two turns then stopping). Infinite loops require explicitly addingmotion-loop-infinite.
Adjustments:
- Default duration reduced from 700ms to 300ms, better suited for micro-interaction pacing.
- Added spring easing based on
linear()(spring-smooth/snappy/bouncy/bouncier/bounciest), from kvin.me/css-springs. - All
@keyframeswrapped in@media (prefers-reduced-motion: no-preference)(tailwindcss-motion only wrapped transform classes; color/opacity classes were not wrapped).
Overall, tailwindcss-motion is a general-purpose web animation library. This project trims and enhances it based on its slot architecture, specifically for SVG icon scenarios.
Here are all currently supported animation effects:
| Effect | Enter | Exit | Loop | Notes |
|---|---|---|---|---|
| Translate | Fly in from top/bottom/left/right | Fly out to top/bottom/left/right | Float up/down/left/right | Supports reverse direction |
| Scale | Scale from small to large | Scale from large to small | Pulse, breathe effect | Supports single-axis scaling |
| Rotate | Rotate in | Rotate out | Continuous rotation | Supports reverse rotation |
| Shake | Shake in | Shake out | Continuous shake | Damped oscillation effect |
| Fade | Fade in | Fade out | Blink | — |
| Stroke Draw | Line drawing in | Line erasing | Line breathing | SVG path animation |
Supports 22 easing curves, including standard easing, back, spring, and bounce effects. Default duration is 300ms, with configurable delay, loop count, and transform origin.
3.2 The Animation Slot Solution in tailwindcss-motion
tailwindcss-motion's core design solves a key problem: how to make multiple animation utility classes coexist without overriding each other. (Thanks to @romboHQ for this contribution.)
Writing two animations for an element is easy in native CSS:
.element {
animation:
scale-in 0.3s,
rotate-in 0.3s;
}But if these two animations need to be composed via utility classes (animate-scale-in animate-rotate-in), CSS cascade logic causes the later animation declaration to completely override the former, rather than appending.
A bigger problem is combining entrance animations with loop animations. An element first flies in from the left (in), then continues to move subtly in place (loop). in is one-shot; loop is continuous. Using two independent class names — CSS doesn't support this, because the animation property is replaced as a whole, not appended. This is a limitation at the CSS specification level.
3.3 Animation Slots
tailwindcss-motion's solution is to pre-declare all slots. Each utility class only writes animation values into its corresponding slot, filling the remaining slots with none. Essentially, it uses CSS variable fallback mechanisms for multiplexing, which works perfectly in animation scenarios:
| Phase | Slots | Properties |
|---|---|---|
| enter | 1–6 | scale-in, translate-in, rotate-in, opacity-in, bg-color-in, draw-in |
| exit | 7–12 | scale-out, translate-out, rotate-out, opacity-out, bg-color-out, draw-out |
| loop | 13–18 | scale-loop, translate-loop, rotate-loop, opacity-loop, bg-color-loop, draw-loop |
6 properties × 3 phases = 18 slots. The concept is as follows:
@utility motion-translate-x-in-* {
--motion-translate-in-animation: /* actual animation value */;
animation:
/* slot 1-6 (enter) */
var(--motion-scale-in-animation),
/* → none */ var(--motion-translate-in-animation); /* → actual animation */
/* ... remaining 16 slots follow the same pattern */
}Actual code (Tailwind v4 @utility):
@utility motion-translate-x-in-* {
--motion-origin-translate-x: --value(--motion-translate-*, [percentage], [length]);
--motion-translate-in-animation: motion-translate-in
calc(
var(--motion-translate-duration, var(--motion-duration)) *
var(--motion-perceptual-duration-multiplier)
)
var(--motion-translate-timing, var(--motion-timing))
var(--motion-translate-delay, var(--motion-delay)) both;
animation:
var(--motion-scale-in-animation), var(--motion-translate-in-animation),
var(--motion-rotate-in-animation), var(--motion-opacity-in-animation),
var(--motion-background-color-in-animation), var(--motion-draw-in-animation),
var(--motion-scale-out-animation), var(--motion-translate-out-animation),
var(--motion-rotate-out-animation), var(--motion-opacity-out-animation),
var(--motion-background-color-out-animation), var(--motion-draw-out-animation),
var(--motion-scale-loop-animation), var(--motion-translate-loop-animation),
var(--motion-rotate-loop-animation), var(--motion-opacity-loop-animation),
var(--motion-background-color-loop-animation), var(--motion-draw-loop-animation);
}All utility classes share the same 18-slot animation declaration. When motion-scale-in-75 motion-translate-x-loop-25 are used together, each class only writes values into its own slot without overriding each other.
The loop slots use animation-composition: accumulate, allowing loop animation transforms to accumulate on top of the final state of the entrance animation, rather than starting from zero. This way, elements can complete their entrance animation first, then continue looping from their current position, with both phases transitioning naturally.
3.4 Arbitrary Values
When preset values aren't enough, you can use bracket syntax to specify arbitrary CSS values:
<g class="motion-translate-x-in-[12px] motion-rotate-loop-[30deg]">
<path d="..." pathLength="1" />
</g>Tailwind v4's --value() extracts values from class names and maps them directly to CSS variables.
3.5 Accessibility
All keyframes are wrapped in @media (prefers-reduced-motion: no-preference). When users disable animations in system settings, animations automatically become silent.
3.6 Trigger Methods
Once animations are defined, you need to control when they play. Atomic animations default to playing entrance animations automatically when elements mount, with loop animations following immediately. But in real-world scenarios, hover triggers are the most common use case — when the user's mouse enters an icon, the entrance animation plays; when it leaves, the exit animation plays.
The design principle here is to keep logic in the CSS layer as much as possible. The trigger mechanism is implemented via the data-motion-trigger attribute. CSS attribute selectors control animation start/stop states:
[data-motion-trigger="hover"]:hover {
--motion-trigger: running;
}Correspondingly, when used in React, motion-react provides a minimal hook:
import { useHoverMotionTrigger } from "motion-react/hooks";
function MyIcon() {
const hoverProps = useHoverMotionTrigger<HTMLDivElement>();
return (
<div {...hoverProps}>
<SettingsIcon />
</div>
);
}useHoverMotionTrigger does something very simple: it returns a ref and a data-motion-trigger="hover" attribute, leaving everything else to CSS. No mouseenter/mouseleave listeners, no state toggling, no JS animation logic.
The benefit of this approach is zero runtime overhead — animation playback and pause are entirely handled by the browser's CSS engine, without triggering React re-renders or requiring JS to manually calculate animation progress. This is especially important for scenarios with 2,000+ icons appearing simultaneously in the sidebar.
IV. Project Structure
This project is a monorepo containing three parts:
css-motion-system/
├── apps/studio/ # Next.js web application
├── packages/
│ ├── motion/ # Pure CSS animation library
│ └── motion-react/ # React icon componentsThe dependency chain is studio → motion-react → motion, one-way with no cycles.
motion has no build step; CSS is published directly. motion-react is bundled via tsup for ESM/CJS and type definitions. The studio hosts the AI workflow. Code formatting and linting use Oxfmt + Oxlint (Rust implementations).
V. Web UI Workflow

The Studio application breaks icon animation into four steps. The core idea is that AI and humans each do what they do best: fully automated AI quality is unstable, and pure manual workshop methods don't scale. Combining both is the viable path.
Step 1: Upload SVG. Drag and drop an SVG file.
Step 2: SVGO optimization + semantic grouping. SVGO performs standardized cleanup: removing redundant nodes, unifying attribute order, adding pathLength="1" to paths (preparing for stroke animations). Then AI steps in, using Gemini 3.1 Flash for multimodal analysis, taking SVG source code and rendered images as input, and outputting grouping suggestions. For example, for a gear icon, AI can identify two semantic groups: "outer teeth" and "center hole":
<svg viewBox="0 0 24 24">
<g data-group="outer teeth">
<path d="..." />
</g>
<g data-group="center hole">
<circle ... />
</g>
</svg>The editor supports overlay comparison, with different groups shown in different colors, and manual drag-and-drop adjustments. This forms a closed loop of AI rough grouping + human fine-tuning.
Step 3: Generate animation plan. Designers can manually select animation types, duration, easing curves, and loop counts, with all changes previewed in real time.

In this step, AI can also generate three animation plans based on semantics. Taking a gear icon as an example:
- Plan A: Outer teeth rotate slowly (loop), center hole fades in (in)
- Plan B: Entire icon slides in from the left (translate-in), rotates 90° (rotate-in)
- Plan C: Stroke drawing growth (draw-in), with slight scale bounce (scale-in + spring-bouncy)
After selecting a plan, you can still manually adjust: modify animation type, duration, easing curve, loop count — all changes previewed in real time.
Step 4: Export. You can choose to export:
- React TSX component with types,
size/strokeWidthprops, and"use client"at the top for RSC compatibility - Standalone SVG with embedded CSS, usable in non-React scenarios
TSX files are written directly into the motion-react source directory and can be published to npm after building.
VI. Skill Workflow
In addition to the Web UI, the entire workflow is encapsulated as an AI conversational tool. All steps are completed via natural language interaction: paste SVG → SVGO optimization → AI grouping → three plans → selection and fine-tuning → generate TSX → write to component package. No window switching, no uploading or downloading.
Uploading files to a webpage is actually an extra step for developers; pasting SVG code directly in the terminal and getting results is more efficient. Moreover, the Skill can use sub-agents to add animations to multiple icons simultaneously, making it even more efficient.
Skill and GUI workflows complement each other: quickly iterate through plans first, then precisely adjust individual icons.
VII. Summary
This solution features the following:
- Updated and extended the tailwindcss-motion design, making it more suitable for SVG icon animation scenarios
- A motion-react library providing React icon components and hooks
- A Web UI workflow in the studio application, supporting AI-assisted icon animation generation
- An AI conversational workflow supporting completion of all steps via natural language interaction
VIII. Acknowledgments
Thanks to romboHQ's tailwindcss-motion for its contribution to this project, and to Tailwind CSS for the inspiration.