"Does it work with the App Router" is almost always answered yes, and almost always the wrong question. Nearly every React component library works. What differs is how much of your page ends up in the browser bundle because of it, and that is decided by a directive at the top of a file rather than by compatibility.
The actual question
A component library affects an App Router project in three ways.
How much of it is client-only. A library where every component carries "use client" pulls itself and its dependencies into the browser, even for components that render nothing interactive.
Whether it can be composed across the boundary. A well-built client component accepts children, so server-rendered content can be passed into it. One that renders content from props alone forces everything above it to be a client component too.
What it does at build time. A library that needs a runtime style pipeline, or that generates styles by executing code, sits awkwardly in a server-first model. Utility classes do not have that problem, which is most of why the Tailwind-based ecosystem fits the App Router so comfortably.
The families, on those terms
shadcn/ui and the registries. Source in your repository, so you decide the directive per file. This is the most controllable option available, and it is also the one where the responsibility is yours: an installed component often arrives marked client whether it needs it or not.
Radix and Base UI. Interactive primitives are genuinely client components, which is correct. The composition matters: keep the trigger and the panel as client, keep their content on the server, and the server components guide covers pushing that boundary down.
Packaged libraries. Most now ship App Router support, and the shape of the trade is: the entry points are client components, and how well the library composes with server children decides whether that costs you one component or a subtree.
The three things to check before adopting one
Open a component and look for the directive. Then ask whether it is justified: state, effects, refs, browser APIs or event handlers. In most libraries a third of the directives are inherited rather than needed.
Check whether components accept children. It is the difference between a client wrapper and a client page.
Look at what a single import drags in. A card that imports an icon set, a date library and an animation runtime is a bundle problem regardless of how it is rendered.
What to do in practice
Keep pages and layouts as server components. Push interactivity into the smallest possible leaf. Pass server-rendered content into client wrappers as children rather than as props. And check the bundle rather than trusting the compatibility claim, because the claim is nearly always true and nearly always irrelevant.
Two specifics that catch people. Data fetching belongs in the server component, not inside a client child that fetches on mount; that is the pattern that quietly turns a fast page into a spinner. And a provider at the root of the tree makes everything under it client-side, which is worth checking before adding a fourth one.
Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
| shadcn/ui | Owning the directive decision per component | The responsibility is yours too |
| Radix / Base UI | Correct client behaviour for genuinely interactive parts | You place the boundary |
| A packaged library | A documented App Router story maintained by someone else | Their boundary decisions, not yours |
| 21st | Components with the source visible before you install | Many are marked client by default; check before assuming |
Taking one
Every component page has a live preview and the code. Installing goes through the shadcn CLI against our registry:
That key comes from your 21st account, and installs require a membership. Set API_KEY_21ST once in your shell and the command works for anything in the catalogue.
Frequently asked
- Which component libraries work with the App Router?
- Nearly all of them, which is why compatibility is the wrong question. What differs is how much lands in the browser: whether the library marks everything as a client component, whether its components accept server-rendered children, and whether its styling needs a runtime rather than utility classes.
- What should you check before adopting a library in an App Router project?
- Open a component and see whether its client directive is justified by state, effects, refs, browser APIs or event handlers. Check whether components accept children, which is the difference between a client wrapper and a client page. And look at what a single import drags in.
- Where should the client boundary go?
- As low as possible. Keep pages and layouts as server components, push interactivity into the smallest leaf, and pass server-rendered content into client wrappers as children rather than as props. A provider at the root makes everything under it client-side.
- Why is my App Router page still shipping a lot of JavaScript?
- Usually a directive higher up the tree than it needs to be, or data fetched inside a client child on mount rather than in the server component above it. Check the bundle rather than the library's compatibility claim, because the claim is almost always true.



