Everything a hiring manager needs
in under 30 seconds.
- 4 concurrent client projects, all needing the same animations — rebuilt from scratch each time
- No source of truth for motion. Design tokens in Figma, CSS scattered across 4 codebases
- Existing libraries (Animate.css, GSAP, Framer Motion) couldn't solve all requirements
- Designed and engineered a production CSS animation library from scratch
- 3-layer composable architecture: CSS core + scroll engine + interactive physics
- Published to npm with CI/CD, TypeScript types, and a backward compat gate
- Designed the DX first:
class="sds-fade-up"and it works. Zero setup. - Backward compatibility = a product promise to every dev who adopted it
- Open source adoption (1,857 organic downloads) as external market validation, without a launch campaign
- 8 verification gates — backward compat, reduced-motion, Playwright visual regression
- 1,812 visual regression baselines + 100%
prefers-reduced-motioncoverage (machine-verified) - Core CSS ~95 KB · scroll gate 1.3 KB · physics engine 7 KB
Choose your depth.
Recruiter scan.
- Solo Creator (Designer + Engineer)
- CSS Core, JS Scroll Engine, Interactive Physics
- Published to npm under `@salkomdesignstudio`
- 600+ animations, 8 verification gates per build
- 1,812 Playwright visual regression baselines
- Angular, React, Tailwind, Web Components wrappers
- 1,857 organic downloads — no marketing budget
- Used in 4 live production client products
- 100% prefers-reduced-motion coverage, machine-verified
Four clients. Four codebases.
The same animation, rebuilt four times.
- Card entrance: different easing per project
- One timing change → update 4 codebases manually
- Figma tokens and CSS already drifting. Motion had no chance.
- Inconsistency compounded over months across DigiGold, Aura, and internal SDS tools
- One versioned npm package, consumed by all 4 projects
- Token change propagates to every project on next npm update
- Figma ↔ code token sync via W3C format — single source of truth for motion
- Consistent animation pacing across all client work, version-to-version
The market alternatives didn't solve it either. I needed something framework-agnostic, CSS-first, with scroll gating, zero setup friction, and a token system that bridged Figma and code — none of the existing libraries checked all four boxes.
| Library | CSS-first | Scroll gate | Token system | Framework-agnostic | Zero setup |
|---|---|---|---|---|---|
| Animate.css | |||||
| GSAP | |||||
| Framer Motion | |||||
| SDS Motion Forge |
The decisions that made this
a library, not a stylesheet.
--sds-duration-fast, --sds-ease-spring — bridging Figma and code in W3C Design Token format cannot. The library and the design system speak the same language. That's what separates a utility file from a design system component.Three layers. Load only
what you use.
The layers are additive — the CSS core is the only required layer. Everything above is opt-in. A site that needs only entrance animations loads ~95 KB of CSS and nothing else. Scroll reveals add 1.3 KB. Cursor-reactive physics adds 7 KB. You pay only for what you use.
prefers-reduced-motion enforced at build level — 100% coverage, machine-verified. Tree-shakeable category bundles via separate entry points.IntersectionObserver-based. Watches all [data-sds] elements via a shared observer. Adds .sds-active on viewport entry. Supports data-sds-threshold, data-sds-root-margin, and .sds-once. Zero-config — drop in the script tag and it auto-initialises.requestAnimationFrame loop.useSdsInView hook. Web Components custom element. Tailwind v4 CSS-first plugin. Full TypeScript union types with IDE autocomplete.Three consumption modes
<p class="sds-velvet-drop">Animates on load. Zero JavaScript.</p>
<!-- Mode 2 — scroll-gated via data-sds -->
<section class="sds-scroll-fade-up" data-sds>Animates when entering viewport.</section>
<!-- Mode 3 — modifier stack -->
<div class="sds-card-lift sds-delay-200 sds-ease-spring sds-once" data-sds>
200ms delay · spring easing · plays once on scroll — no JavaScript
</div>
<!-- Mode 4 — stagger children automatically -->
<ul class="sds-stagger">
<li class="sds-scroll-fade-up" data-sds>Item 1</li>
<li class="sds-scroll-fade-up" data-sds>Item 2</li>
</ul>
:root {
/* 6-step duration scale — override at :root to retime all 300+ animations */
--sds-duration-instant: 0.12s;
--sds-duration-fast: 0.25s;
--sds-duration-normal: 0.4s;
--sds-duration-moderate: 0.6s;
--sds-duration-slow: 0.9s;
--sds-duration-xslow: 1.4s;
/* Named easing — not raw bezier values */
--sds-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--sds-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--sds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
}
Three major wrong turns
before the architecture that worked.
motion.css, copied manually into each project. No versioning. No API contract. No way to update across projects without risking regressions. The problem wasn't the animations — it was that this was a shared stylesheet, not a library.Eight gates. Every build.
Backward compat machine-verified.
The zero breaking changes metric isn't a promise I keep by memory. Every build runs 8 verification gates before output is considered safe to publish — including a stage that compares the build against the published npm tarball and fails hard if any class is missing or changed.
# Download previous published tarball from npm registry
- name: Check backward compatibility
run: |
# Extract class names from published tarball
npm pack @salkomdesignstudio/sds-motion-forge --dry-run \
| grep '\.sds-' > published-classes.txt
# Extract new build classes
grep '\.sds-' dist/**/*.css > new-classes.txt
# Fail if any published class is missing from new build
comm -23 <(sort published-classes.txt) <(sort new-classes.txt) \
| grep '.' && exit 1 || echo "✓ All classes preserved"
If a class is accidentally removed — even by renaming an internal file — CI fails with a diff of missing classes. A separate Playwright visual regression suite compares 1,812 baseline screenshots on every build. The Angular directive uses signal inputs so if animation changes at runtime, the class updates reactively. JS adds a class. CSS does the animation.
Lighthouse Speed Index under 1.2s, zero blocking JS.
WCAG 2.2 AA compliant contrast, aria attributes verified.
CLS: 0.00, LCP: 0.8s, INP: 45ms in simulated user loops.
100% test coverage across core animations & directive triggers.
Five real bugs.
Five real fixes.
:nth-child() patterns hardcoded to 8. Lists with more than 8 items had all remaining elements fire simultaneously — no stagger. Looked broken on any real navigation list or card grid.
Expanded to 12 and introduced sds-stagger-auto using CSS custom property indexing via --sds-child-index — no JavaScript required.
Inside containers with overflow: hidden — standard in Angular Material components and Bootstrap cards — clip-path animations were completely invisible. No error. Just nothing happening. Root cause: overflow: hidden clips content starting outside parent bounds before the animation begins.
Added sds-clip-safe — a utility class that sets overflow: visible on the direct parent scope. Documented which animation categories require it with clear error guidance in the README.
transform: translateY() and scale() have no effect on display: inline elements. This means applying sds-fade-up to a <span> or <a> silently produced no animation — developers would file it as a library bug.
Added display: inline-block to every animation class that uses transforms, auditing all 600+ effects. Now works on any element regardless of its default display value.
Listed @salkomdesignstudio/sds-motion-forge as a devDependency in its own package.json for integration testing. npm's install algorithm detected the circular reference and failed with a lockfile resolution error on fresh installs.
Moved all self-testing to a separate packages/test-harness workspace that declares the package as a local path dependency using "file:../../". The published package has zero self-references of any kind.
Version 5 reorganised the entire internal CSS architecture — categories merged, keyframe names changed, file structure completely different from v3. But clients had v3 class names in production Angular and Flutter codebases I did not control. Breaking those classes would mean breaking live client products.
The backward compatibility CI gate caught every missing class during the build. For each removed class, created a legacy alias in legacy/v3-aliases.css using PostCSS composes. Zero duplication in output, no runtime cost, no breaking change.
Numbers that prove
it solves a real problem.
Growth achieved entirely through organic discoverability, technical documentation, GitHub visibility, npm search indexing, and SEO metadata optimization. No marketing spend.
v3 through v5. Machine-verified at every push. Aggressive internal refactoring without breaking consumer projects.
DigiGold (Pothys, GRT, Lalitha, Super Saravana Stores), Aura Elite Events, and internal SDS tooling. Pays back investment in hours saved.
Complete token scale in W3C Design Token format, importable by Figma Token Studio. Design and code share the same source of truth for motion timing.
What building for other developers
taught me about product design.
Build for the developer who will not read your documentation
The most common complaint about animation libraries is setup complexity. If the first step after npm install is "now import this, register that, and wrap your component in a provider," you have lost most of your potential users. My single-class API came from one question: what does the laziest possible integration look like? Whatever that is, make it work correctly. Then build everything else on top of it.
Backward compatibility is a product decision, not just an engineering one
When I committed to never removing a v3 class, I was making a product promise to every developer who used the library. Breaking that promise — even for a "cleaner" internal architecture — costs trust that takes versions to rebuild. The machine-verified gate is how I honour the promise without relying on my own memory or discipline in the moment. Automate your constraints.
Open source is a maintenance surface, not a launch moment
Publishing to npm is not shipping. Shipping is maintaining: answering issues, updating the build pipeline when a dependency changes, ensuring CI passes when GitHub Actions updates its runner. The difference between a package with 12 downloads and one with 1,857 downloads is not the launch announcement. It is continued investment in reliability, backward compatibility, and documentation that does not go stale.