/

Component Libraries for the Next.js App Router

Compatibility is the wrong question, because nearly everything works. What differs is how much of your page ends up in the browser.

Serafim Korablev
Serafim Korablev
@korablev

"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:

SourceBest forTrade-off
shadcn/uiOwning the directive decision per componentThe responsibility is yours too
Radix / Base UICorrect client behaviour for genuinely interactive partsYou place the boundary
A packaged libraryA documented App Router story maintained by someone elseTheir boundary decisions, not yours
21stComponents with the source visible before you installMany 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:

bash

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.

Browse components →

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.

Published

Aug 21, 2026

Read time

4 min

Tags

GuideNext.jsReactPerformance

Share