Dec 10 2025 · Motion · React · Tailwind v4

A tag picker that morphs through its states

A multi-view popover that animates through list → pick-color → creating → created. Built originally for Stax.ai; stripped here to the parts that make it feel alive.

designresearch
Add an existing tag, or type a new one and pick its colour. Everything you see is the same surface animating through four views.

The shape of the problem

Most popovers do one thing. Search a list. Pick a value. Confirm an action.

A tag picker has to do four things, in sequence, on the same surface. The user starts in a list of existing tags. They might type something that exists, or something that doesn't. If it doesn't, they're now creating it — and the picker has to ask them what colour the new tag should be. After they pick, it has to confirm. After it confirms, it has to drop them back into the list with the new tag included.

You can solve this with four separate popovers. Or you can make one popover that morphs.

The four views

The state machine is small enough to write out:

The first time the picker enters a non-list state, a thin header strip slides in above the input. That strip is the surface that does most of the animation work — its inner content cross-fades as the state changes, but the strip itself stays the same shape.

What the animations are doing

The header strip animates height from 0 to auto with a soft spring ({ type: "spring", duration: 0.4, bounce: 0.15 }). Soft, not bouncy. It needs to feel decisive — the picker has changed mode — without overshoot.

Inside the strip, three motion nodes (one per non-list state) are wrapped in <AnimatePresence mode="wait">. As state advances, the outgoing node fades to opacity 0, completes, then the incoming node fades in. The 180ms duration is short enough that the user reads the change as instant, but long enough that the eye registers the transition rather than a hard cut.

The created state's badge animates from scale(0.95) instead of from scale(0). Real things don't appear from nothing. The check icon is a small flourish — a success moment that takes a beat — and then the popover dismisses.

Removing a tag uses the same <AnimatePresence> boundary as the list mounting. Each badge has layout enabled, so the remaining tags reflow smoothly when one is removed rather than snapping.

What it's stripped of

The version in the production app talks to a companyKeys cache, fires through React Query mutations, hits a Stax API to create the tag on the user's company, validates against a label registry, and surfaces a Sonner toast on failure. None of that is here. What's left is the part that matters for the feel — the same component, same state machine, same timings, with setTimeout standing in for the network and a local array for the persistence.

That's a useful framing for a portfolio craft piece. The animation choices and the state machine were the part I cared about. Everything else was scaffolding for those choices.

The point

Multi-view popovers are unusual in production UIs because most teams don't take the time to design the morph between states. They settle for two clicks where one would have done. The work here was small — five hundred lines, two springs, three keyed motion nodes — and it makes the difference between a feature that feels like infrastructure and a feature that feels like product.

It's the kind of thing another developer would have shipped in a day. Worth taking the afternoon.