Component Libraries
Building a polished, accessible UI from scratch is a surprising amount of work: focus management, keyboard navigation, ARIA wiring, theming, and responsive layout all add up before you’ve shipped a single feature. Component libraries let you borrow that work. The React ecosystem offers three distinct flavors—fully styled kits, unstyled headless primitives, and copy-in source—and picking the right one shapes how much control you keep and how much you can delegate.
The three families
Most React UI libraries fall into one of three categories, and the difference is mostly about who owns the styling.
- Full component kits ship pre-styled, ready-to-use components. You import a
Buttonand it already looks good. Examples: MUI (Material UI), Mantine, Chakra UI. - Headless libraries give you behavior and accessibility with zero visual styling. You bring your own CSS. Examples: Radix UI, Headless UI, React Aria.
- Copy-in distributes component source code into your repo rather than a versioned dependency. You own and edit the files. Example: shadcn/ui (built on Radix + Tailwind).
Full component kits
A full kit is the fastest way to a coherent-looking app. You install one package, wrap your tree in a theme provider, and start composing.
// MUI
import { ThemeProvider, createTheme, Button, TextField, Stack } from "@mui/material";
const theme = createTheme({ palette: { primary: { main: "#5b21b6" } } });
export default function SignupForm() {
return (
<ThemeProvider theme={theme}>
<Stack spacing={2} sx={{ maxWidth: 360 }}>
<TextField label="Email" type="email" required />
<Button variant="contained">Create account</Button>
</Stack>
</ThemeProvider>
);
}
Mantine and Chakra follow the same shape with their own theming systems—Mantine leans on CSS variables and a rich hooks library, while Chakra uses a style-props API. The tradeoff with kits is opinionation: theming a Material-styled Button into something that doesn’t look like Material can mean fighting the framework.
Headless libraries
Headless primitives invert the model. Radix UI, for instance, handles the hard accessibility logic of a dialog—focus trapping, Escape to close, scroll locking, ARIA roles—but renders unstyled elements you style however you like.
import * as Dialog from "@radix-ui/react-dialog";
export function ConfirmDialog() {
return (
<Dialog.Root>
<Dialog.Trigger className="btn">Delete</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="modal">
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>This action cannot be undone.</Dialog.Description>
<Dialog.Close className="btn">Cancel</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Headless UI (from the Tailwind team) covers a smaller set of components and pairs naturally with Tailwind. React Aria (Adobe) goes deepest on accessibility and internationalization, exposing hooks like useButton and useDatePicker for teams that want total rendering control.
Copy-in: shadcn/ui
shadcn/ui isn’t a dependency you install—it’s a CLI that generates component source into your project, built on Radix primitives and styled with Tailwind. Because the code lives in your repo, you edit it directly instead of overriding it from the outside.
$ npx shadcn@latest add button dialog
Output:
✔ Checking registry.
✔ Created components/ui/button.tsx
✔ Created components/ui/dialog.tsx
You then import from your own tree, and customization is just editing a local file.
import { Button } from "@/components/ui/button";
export function Toolbar() {
return <Button variant="outline">Export CSV</Button>;
}
The flip side of owning the source: updates aren’t automatic. Re-running the CLI for a component overwrites your edits, so track customizations in version control and review diffs carefully when upgrading.
How to choose
| Need | Best fit |
|---|---|
| Ship an internal tool or MVP fast | MUI or Mantine |
| Design-system-grade visual control | Radix or React Aria + your CSS |
| Tailwind project, want to own the code | shadcn/ui |
| Smallest bundle, minimal components | Headless UI |
| Strict accessibility / i18n requirements | React Aria |
| Library | Type | Styling | Bundle weight | A11y depth |
|---|---|---|---|---|
| MUI | Full kit | Built-in (emotion) | Heavy | Good |
| Mantine | Full kit | Built-in (CSS vars) | Medium | Good |
| Chakra UI | Full kit | Built-in (style props) | Medium | Good |
| Radix UI | Headless | None | Light | Excellent |
| Headless UI | Headless | None | Very light | Good |
| React Aria | Headless | None | Light | Best-in-class |
| shadcn/ui | Copy-in | Tailwind | You control | Excellent (Radix) |
Accessibility notes
Accessibility is the strongest argument for adopting any of these libraries. Headless options like Radix and React Aria treat WAI-ARIA patterns as the product, not an afterthought—correct roles, keyboard interactions, and focus management come for free. Full kits are generally solid too, but verify the specific components you use; a styled Menu is only accessible if its underlying behavior is. Whatever you pick, still test with a screen reader and keyboard-only navigation, because composition and custom styling can quietly break what the library got right.
Best practices
- Pick one primary library per project; mixing several styling systems bloats your bundle and fragments your visual language.
- Prefer headless or copy-in libraries when you have a real design system—you keep full control without reimplementing accessibility.
- Wrap third-party components in thin local wrappers so swapping libraries later touches one file, not hundreds.
- Audit bundle impact with your build tool’s analyzer before committing to a heavy kit.
- Lean on accessible primitives instead of hand-rolling dialogs, menus, and comboboxes—these are where homegrown code most often fails.
- Keep theming centralized (theme provider or CSS variables) rather than scattering inline overrides.
- For copy-in libraries, commit generated source and review upgrade diffs deliberately.