Nobody tells you an interface feels cheap.
They just leave.
A visitor doesn't run through a checklist. They land, move the cursor, click something — and somewhere in that first half-minute they've already decided whether this is worth trusting with the next ten.
Nobody writes that verdict down. They just act on it, usually before they've read a single word.
The gap
Ask someone why an app felt better than a website and they will say it was faster, or smoother, or just nicer. It is usually none of those. A native control tracks your finger the whole time it is moving, can be grabbed in the middle of its own animation, and settles instead of stopping. Most web components fire once on a click event, run a CSS transition that cannot be interrupted, and are finished deciding before your hand is.
That is the whole difference, and it is a difference in what the component is listening to — not in frame rate, and not in how it looks in a screenshot.
What stops listening
Three symptoms of the same cause. Each takes seconds to find in an interface you already use.
- 1
It commits the moment you press
You start a drag and the component has already decided where it is going. Nothing you do for the next three hundred milliseconds changes the outcome. A native control is still reading your finger the entire time, which is why it feels like you are moving the thing rather than requesting that it move.
- 2
It cannot be interrupted
Open a panel, change your mind, close it half-way through. The web version jumps to fully-open and then plays the close from rest. The native one turns around from wherever it actually is. This is the single clearest tell, and it takes about a second to find in any interface.
- 3
Nothing happens until you let go
The gesture runs, the screen holds still, and the result appears on release. Every frame in between was a chance to show what was about to happen, spent on nothing. Feedback that arrives only at the end is indistinguishable from a button that took a while.
What nobody measured
The quieter half. None of these are about motion at all, and none of them are one broken thing — they are decisions nobody made on purpose, each too small to file as a bug, that add up to a feeling anyway.
- 4
The layout jumps
A card finishes loading and the paragraph underneath drops to make room for it. The button you were about to hit is now hovering over something else entirely. Every state an element can reach needs its space reserved before it gets there — not the moment after.
- 5
The hover answers late
You put the cursor on a button and, for a beat, nothing happens — then the color catches up once you've already moved on. Feedback under a tenth of a second reads as instant. Anything slower reads as absence, even once it arrives.
- 6
Nothing was actually measured
Twenty pixels here, eighteen there, a heading sized at whatever the framework shipped with. No single gap is wrong enough to flag on its own. The page just feels slightly off, in a way nobody can point to — because it was never built from one scale to begin with.
The test
Nothing gets built here on the strength of looking good. A component has to survive one question: describe how it behaves under the hand, in one sentence — what it does while you drag it, and what happens if you let go halfway.
“A fresh take on tabs” says nothing and fails. “You drag across the tabs and the content follows your finger, and releasing halfway springs it back” passes — and it also tells you what to build. Every description in this registry is written that way on purpose. If one of them reads like a category name rather than a behaviour, the component underneath it is not finished.
The thing being competed with is the native app, not another registry. That is a high bar and it is meant to be.
How it ships
Each component rebuilds the actual mechanism rather than the look of it: a spring that answers in under a tenth of a second and adapts if you interrupt it instead of snapping back to start, a gesture that reverses from where it is, surfaces that visibly step up off the page instead of a border nobody notices, a type and spacing scale where every size traces back to one system. None of it is supposed to read as an effect. That restraint is the craft, not the absence of it.
Then it ships as source, the same distribution model as shadcn/ui: code that lands directly in your project and becomes yours to maintain, not a package you install once and never open again. The hard part is meant to live inside the component, so that using it correctly never requires knowing why it was built this way.
Browse components