Skip to content
All work
Design2026 Case study

Ansh

A framework-agnostic animation engine and component library for React, Next.js, Vue and Nuxt.

ansh.techiesofepi.com
Ansh landing page
Components
57
Easing curves
40
Runtime dependencies
0

The challenge

Motion and component libraries almost always start in one framework and get ported to the rest — usually by a different team, on a different schedule. A spring that feels right in the React build drifts in the Vue port, a focus trap that works in one forgets the other, and six months later nobody can say which implementation is correct.

Our solution

We inverted the usual order and built the engine first: @ansh-ui/core owns every spring, easing curve and colour computation with no React or Vue in it at all. The React and Vue packages are thin bindings over that same engine, built and tested together, so there is no primary framework for the others to fall behind. Springs are solved in closed form, colour is computed in OKLCH, and one ticker drives every animation against an adaptive frame budget.

Built withTypeScriptReactVueNuxtOKLCHCanvas

How we built it

The approach.

  1. 01

    Build the engine before either framework

    The first package written was the one with no UI framework in it at all. Every decision about how a spring settles, how an easing curve is shaped and how a colour is derived lives in @ansh-ui/core, in plain TypeScript, testable without mounting a component.

  2. 02

    Make the bindings thin on purpose

    The React and Vue packages translate between the engine and their framework — lifecycle, refs, reactivity — and nothing more. When a binding starts wanting its own copy of a behaviour, that is treated as a signal the behaviour belongs in the engine.

  3. 03

    Ship both frameworks together

    Neither React nor Vue is the reference implementation. They are built and tested in the same pass, so there is no window in which one is ahead and the other is catching up.

  4. 04

    Solve motion analytically

    Springs are evaluated in closed form at a given time rather than stepped forward frame by frame, which removes frame rate from the equation entirely and makes the same animation reproducible in a test.

  5. 05

    Schedule everything on one clock

    A single ticker drives all active animations against an adaptive frame budget, so a page with fifty animated elements schedules like one animated page rather than fifty competing loops.

The problem with porting

Component and motion libraries almost always begin life in one framework. The React version ships first, gets used, gets refined. Some months later a Vue port arrives — written by different people, on a different schedule, against a specification that mostly lives in the original source code.

What follows is predictable. A spring that feels right in the React build drifts in the Vue port, because someone re-implemented the physics from the outside and got close rather than exact. A focus trap handles a case in one framework that the other has never heard of. Six months on, the two implementations have diverged enough that nobody can say with confidence which one is correct — and every bug report has to be triaged twice.

The root cause is structural, not a matter of discipline. If the behaviour lives inside framework-specific code, then supporting a second framework means duplicating the behaviour, and duplicated behaviour drifts. No amount of care prevents that; it only slows it down.

Inverting the order

Ansh was built the other way round. The engine came first.

@ansh-ui/core contains every spring, every easing curve and every colour computation, and it imports neither React nor Vue. It is not a shared utilities package that grew out of the React build — it is the actual implementation, and the framework packages are bindings over it.

That distinction matters more than it sounds. A utilities package is something a framework implementation uses; the interesting logic still lives upstairs, and a second framework re-implements that logic. An engine is where the logic is; a binding has nowhere to put a divergent copy, because there is nothing left to diverge about. When a React binding and a Vue binding both ask the engine where a spring should be at 120 milliseconds, they get the same number, because it is the same function.

The bindings handle exactly what is genuinely framework-specific: lifecycle, refs, reactivity, and how a component is described in that ecosystem. Everything else is delegated. And because there is no primary framework, there is no port to fall behind — React and Vue are built and tested in the same pass.

Motion that does not depend on the frame rate

Most animation libraries integrate a spring numerically: take the current position and velocity, step forward by however long the last frame took, repeat. It works, and it is subtly frame-rate dependent. The same animation on a 60Hz laptop, a 120Hz phone and a machine dropping frames under load produces three slightly different curves, and it is not reproducible in a test — which is why so few motion libraries have meaningful tests of their motion.

Ansh solves springs in closed form instead. Given the spring's parameters and a time, it evaluates the position directly. Frame rate stops being an input. The same animation is identical everywhere, replaying it produces the same numbers, and a test can assert on the value at 200ms without rendering anything.

Forty easing curves ship alongside, defined in the engine so both frameworks reference the same definitions rather than each carrying a table of approximations.

Colour computed in OKLCH

Component libraries generate colour constantly — a hover state, a disabled state, a border derived from a fill. Do that arithmetic in the colour space the web has traditionally used and the results are uneven in a way that is hard to name but easy to see: lightening a yellow and lightening a blue by the same amount produces two changes that do not feel equal, because the space is not perceptually uniform.

OKLCH is. Working in it means a derived state is as far from its base colour as the numbers say it is, consistently across every hue. Themes generated from a single seed colour stay balanced, and the components in the library stay coherent with each other rather than needing per-hue corrections.

One clock for the whole page

The natural way to animate a component is to have it request its own animation frames. It works fine for one component and degrades as the page fills up: fifty animated elements means fifty independent loops, each unaware of the others, all competing for the same frame.

Ansh runs a single ticker that drives every active animation and works to an adaptive frame budget. Scheduling is a property of the page rather than of each component, which is what allows a lot of motion to coexist without any individual piece needing to know how busy the page is.

Where it landed

57 components. 40 easing curves. Zero runtime dependencies — the engine implements its own maths rather than pulling in a physics or colour library, which keeps the install size predictable and means the behaviour cannot change underneath a consumer because a transitive dependency shipped a minor version.

The result worth naming is not any of those numbers, though. It is that "does this behave the same in Vue?" stopped being a question anyone needs to ask.

Frequently asked questions

What does framework-agnostic actually mean here?

The behaviour lives in a package that imports neither React nor Vue. Springs, easing curves and colour computation are all implemented in the core engine package in plain TypeScript, and the React and Vue packages are thin bindings that translate between that engine and their framework. It is not a shared utilities package alongside two implementations — there is one implementation, used by both.

Why solve springs in closed form instead of integrating them?

Numerical integration steps a spring forward by however long the last frame took, which makes the resulting curve depend on the frame rate and impossible to reproduce in a test. Evaluating the spring analytically at a given time removes frame rate from the calculation, so the animation is identical on a 60Hz laptop and a 120Hz phone, and a test can assert on its value at a specific moment.

Why compute colour in OKLCH?

Because component libraries derive colours constantly — hover states, disabled states, borders from fills — and colour spaces that are not perceptually uniform make those derivations uneven across hues. In OKLCH, the same adjustment applied to a yellow and to a blue produces changes that look equally large, so generated states stay balanced without per-hue corrections.

Why does a single ticker matter?

If every animated component requests its own animation frames, a busy page ends up with dozens of independent loops competing for the same frame with no knowledge of each other. One ticker driving all active animations against an adaptive frame budget makes scheduling a property of the page, which is what lets a lot of motion coexist without each component having to account for how busy things are.

Let's build together

Have somethingworth building?

Tell us about it. You'll get an honest read on scope, timeline and cost within one business day — from the people who would actually do the work.

Or email us directly — info@techiesofepi.com