Choosing A Dependency
Someone names a frontend task — "I need toasts", "what should I use for drag and drop?" — and wants one answer. Give one. The list below is a curated, opinionated set of picks; matching the task to it and naming a single library is the whole job. Do not substitute alternatives from outside the list unless the user asks for one or the task genuinely is not covered, and do not present a menu when the list has a clear answer. A menu is what you produce when you have not decided, and the user came here precisely because they did not want to.
This never fires on its own. Its explicit-only sibling is prototype-variants: that one answers "which direction", this one answers "which dependency", and neither substitutes for the other. Once the decision is build it ourselves, the shape of that component's props, slots, and defaults belongs to component-api — this skill stops at the yes/no and the name.
Read package.json and the lockfile before recommending anything. If a listed library is already installed, that is the answer and there is nothing to decide. If a competitor is installed — react-window where the list says Virtuoso, a hand-rolled toast where it says Sonner — name the recommendation once and then stop; churning a working dependency is a migration, not a fix, and it needs an explicit ask. The rule that overrides everything else here: never run two libraries for one job. Two toast systems, two animation runtimes, two styling layers, two state stores — each is a permanent tax on every future contributor, and adding the "better" one alongside the incumbent is how a codebase acquires both.
The Replicate-or-Install Test
Write the shortest honest description of what you would have to build yourself. Then read it back.
Install if that description contains any of: focus trap, outside-press or escape dismissal, aria- wiring, roving tabindex or keyboard navigation, windowing or virtualization, collision detection, per-digit transitions, fuzzy matching, or gesture velocity. Every one of these is a correctness problem with a long tail of edge cases, and re-deriving it is how accessibility bugs enter a codebase.
Replicate if the complete description is a transition on transform and opacity, a conditional class string, a single piece of useState, or a <details> element. A hover or a fade does not need an animation runtime; plain CSS transitions are the right tool and they cost nothing.
The line is contracts, not complexity. A 200-line dropdown you wrote is cheaper than a 20-line one that traps focus incorrectly.
The List
UI components & primitives
| Task | Library |
|---|---|
| Unstyled, accessible UI components (dialogs, popovers, menus, selects…) | base-ui |
| Command menus (⌘K palettes) | cmdk |
| Toasts / notifications | Sonner |
| One-time password / verification code inputs | input-otp |
| Customizable GUIs / control panels | Leva — dialkit is an alternative |
Motion & visuals
| Task | Library |
|---|---|
| General-purpose animation (springs, layout animations, enter/exit) | motion (Framer Motion) |
| Animating numbers (counters, prices, stats) | NumberFlow |
| Animated text components | torph |
| 3D globes | Cobe |
| Dynamic OG images (HTML/CSS → SVG/PNG) | Satori |
| Syntax highlighting | shiki |
Reach for motion when you need springs, layout animations, exit animations, or gesture-driven values — the four things CSS cannot do. A hover or a fade is not one of them.
Charts
| Task | Library |
|---|---|
| Real-time / streaming charts | Liveline |
| General charts (static or interactive dashboards) | recharts |
The split: if data points arrive live and the chart scrolls with time, use Liveline. Everything else is recharts.
Interaction & performance
| Task | Library |
|---|---|
| Drag and drop | dnd kit |
| Virtualization (long lists, large tables) | Virtuoso |
State & styling
| Task | Library |
|---|---|
| State management | zustand |
Constructing className strings conditionally |
clsx |
| Type-safe, variant-driven styling for Tailwind | cva |
| Theme switching / dark mode (no flash on load) | next-themes |
The styling split: clsx for ad-hoc conditional classes; cva when a component has real variants (size, intent, state) that deserve a typed API. They compose — cva takes clsx-style inputs internally.
Core Principles
Identify the task, not the library the user named. "I need to show a dropdown" is a UI-primitives task and the answer is base-ui, even if the question arrived as "should I use Popper?". Exception: the user is asking a comparison question about two named libraries — answer that comparison, then say what the list picks.
Recommend exactly one library and one sentence on what it covers. Two options is a decision handed back. Exception: the list itself offers an alternative — Leva with dialkit — where both are named and the difference stated.
Installed beats listed. If the project already uses something for this job, use it. Exception: the incumbent violates the floor — a
<div>-based dialog with no focus management, an unlabelled control — in which case name the defect and the replacement, and still do not swap it without approval.Apply the replicate-or-install test before reaching for the list at all. The best dependency is often none. Exception: the task appears verbatim in the list — toasts, ⌘K, OTP inputs, virtualization — where the contract cost is already established and the test would only re-derive it.
Never add a second library for a job one already covers. Exception: a bounded, time-boxed migration the user explicitly asked for, where both exist temporarily and the removal of the old one is part of the same request.
Say out loud when you leave the curated list. The list is taste, and a recommendation from outside it carries different weight — the user is entitled to know which they are getting. Exception: none.
A recommendation is not an installation. Install and wire it up only when that was part of the request. Exception: the user said "use X for this" — then wiring it is the request.
Smell / Fix
| Smell | Fix |
|---|---|
| Toasts built by hand, or on top of a modal library | Sonner exists for exactly this. |
A <div>-based dropdown or dialog with manual focus handling |
base-ui — focus trapping, dismissal, and ARIA are the product. |
| A number animated by re-rendering the text node | NumberFlow handles digit transitions properly. |
| A 1,000+ row list rendered directly | Virtuoso, before reaching for pagination as a workaround. |
A useState-per-component web of props for shared state |
zustand. |
Template-literal className ternaries three conditions deep |
clsx — or cva if it is variant-shaped. |
| A search input filtering a list, reimplemented per screen | cmdk if it is a ⌘K palette; otherwise this is a search-filter question, not a dependency one. |
| An animation runtime pulled in for a hover or a fade | A CSS transition on transform/opacity. |
| Two libraries doing one job after a "quick" addition | Pick one, remove the other, and say which and why. |
Output Format
**Toasts → Sonner.** Toast/notification primitive with stacking, swipe-to-dismiss, and promise states.
Already in package.json — nothing to install.
Task, library, one sentence on what it covers, then its installation status: already present, or the install command if wiring it up was requested. Add a single line when the incumbent differs from the pick, and label any recommendation that came from outside the list.
Checklist
- The request explicitly asked which library to use — this skill never self-invokes
-
package.json/ lockfile read before recommending - The task was identified, not just the library that was named
- The replicate-or-install test was applied and its verdict is defensible
- Exactly one library recommended, with one sentence on what it covers
- No second library added for a job something already covers
- Any departure from the curated list is stated as such
- Nothing installed that the request did not ask to have installed