React UI Components Complete Reference: 60+ Patterns with Code
Every React UI component pattern you'll actually use — buttons, cards, tabs, carousels, modals, and 55+ more. Real code, real decisions, no filler. Built on Tailwind v4.0.2.
Why you need a component reference, not a component zoo
Honestly, most "UI component" articles are just a shopping list with screenshots. You get a button, a modal, a tooltip — each in its own isolated demo — and then you're left wondering how they actually fit together in a real app.
This reference is different. Every pattern here maps to a real decision you'll face: when to reach for a headless primitive vs. a styled component, how to handle keyboard navigation without wiring it yourself, which animation approach won't tank your Lighthouse score. We're covering 60+ component patterns across 10 categories, and for each one you'll see real props, real tradeoffs, and at least two code examples that you can actually copy.
We're building on React 19 and Tailwind v4.0.2 throughout. All code examples assume TypeScript with strict mode enabled. Let's get into it.
Buttons and interactive primitives
Buttons are where almost every design system starts going wrong. Teams define one <Button> component, then spend the next six months adding variants, sizes, loading states, icon support, and tooltip wrappers — until the props interface looks like a flight control panel.
The fix is composition over configuration. Start with a base <ButtonBase> that handles focus rings, disabled states, and keyboard events. Then build visual variants on top.
// ButtonBase.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react';
interface ButtonBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
export const ButtonBase = forwardRef<HTMLButtonElement, ButtonBaseProps>(
({ className, asChild, children, ...props }, ref) => {
return (
<button
ref={ref}
className={[
'inline-flex items-center justify-center gap-2',
'rounded-lg px-4 py-2 text-sm font-medium',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
'transition-colors duration-150',
className,
].join(' ')}
{...props}
>
{children}
</button>
);
}
);
ButtonBase.displayName = 'ButtonBase';From ButtonBase you derive your variants: <PrimaryButton> (solid fill), <GhostButton> (transparent with border), <DestructiveButton> (red tones). Each is a thin wrapper that passes the right className.
For animated button patterns — think press feedback, shimmer on hover, magnetic pull — you'll want to add a motion library. Framer Motion's useMotionValue works well here, but keep the animation opt-in so server components can render a static fallback.
One thing people skip: the loading state. Don't swap button text for a spinner — that causes layout shift. Instead, keep the text and overlay the spinner at 50% opacity. Set aria-busy="true" and aria-disabled="true" programmatically. Screen readers will thank you.
Card components and stacking patterns
Cards are the workhorse of SaaS UIs. Product cards, pricing cards, testimonial cards, stat cards — you'll build dozens of variations. The key is keeping them structurally consistent even when visually diverse.
A good card shell has: a container with padding (typically 24px / 1.5rem), an optional header slot, a body slot, and an optional footer slot. Use CSS Grid instead of Flexbox for the shell so that the footer always sticks to the bottom, even when body content is short.
// Card.tsx
interface CardProps {
header?: React.ReactNode;
footer?: React.ReactNode;
children: React.ReactNode;
className?: string;
}
export function Card({ header, footer, children, className }: CardProps) {
return (
<div
className={[
'grid grid-rows-[auto_1fr_auto]',
'rounded-xl border border-white/10',
'bg-white/5 backdrop-blur-md',
'p-6 gap-4',
className,
].join(' ')}
>
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer border-t border-white/8 pt-4">{footer}</div>}
</div>
);
}For stacked or layered card effects — the kind where cards fan out behind the active one — check out cards stack patterns for React. The trick is using translate-y and scale transforms on sibling elements, not z-index shuffling, which is smoother for the GPU.
Glassmorphism cards are worth understanding separately. They use backdrop-filter: blur(12px) and a semi-transparent background like rgba(255,255,255,0.08). If you're curious about the underlying design principle, what is glassmorphism covers the theory in depth. You can also generate values fast with the glassmorphism generator.
Performance note: backdrop-filter creates a new stacking context and forces GPU compositing. On pages with many blurred cards, you can drop frame rates noticeably on lower-end devices. Profile with DevTools before shipping more than 3-4 blurred elements per viewport.
Tab components: controlled vs. uncontrolled
Tabs are one of the most misunderstood components in React. The question isn't "how do I show and hide panels" — that's trivial. The real question is: who owns the active tab state?
Uncontrolled tabs manage their own state internally. Good for simple cases. Controlled tabs expose activeTab and onTabChange props, letting the parent drive the logic. You'll need controlled mode whenever the URL should reflect the active tab (?tab=overview), or when you want to programmatically switch tabs from outside the component.
Accessibility is non-negotiable for tabs. The ARIA pattern requires role="tablist" on the container, role="tab" on each trigger, role="tabpanel" on each panel, and proper aria-selected, aria-controls, and aria-labelledby wiring. Arrow keys must navigate between tabs. Home/End must jump to the first/last tab.
For animated transitions between tab panels, see animated tabs in React — it covers both the slide-in approach (Framer Motion <AnimatePresence>) and the crossfade approach (opacity transition), with performance notes for each.
One subtle gotcha: don't unmount hidden tab panels if they contain forms or expensive data-fetching components. Prefer hidden attribute or display: none over conditional rendering — the panel stays mounted but invisible, preserving its state.
Modals, drawers, and overlay components
Here's the thing: most modal implementations on the web are broken. They don't trap focus, they don't restore focus when closed, and they don't handle the Escape key consistently. Users relying on keyboards or screen readers hit a dead end.
The good news is that the native <dialog> element solves all of this in modern browsers (Chrome 98+, Firefox 98+, Safari 15.4+). It handles focus trapping, backdrop clicks, and Escape by default. Reach for it before building your own focus trap.
// Modal.tsx — using native <dialog>
import { useEffect, useRef } from 'react';
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
export function Modal({ open, onClose, children }: ModalProps) {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = ref.current;
if (!dialog) return;
if (open) {
dialog.showModal();
} else {
dialog.close();
}
}, [open]);
return (
<dialog
ref={ref}
onClose={onClose}
className="rounded-xl border border-white/10 bg-neutral-900 p-6 text-white backdrop:bg-black/60 backdrop:backdrop-blur-sm"
>
{children}
</dialog>
);
}Drawers (slide-in panels from left/right/bottom) follow the same accessibility rules as modals. The difference is the transform animation: translate-x-full → translate-x-0 for a right drawer. Use will-change: transform during the animation, then remove it immediately after to avoid keeping the layer in GPU memory.
Toasts and notification banners are a separate category even though they're also overlays. They should NOT trap focus — they're non-blocking. Use role="status" and aria-live="polite" so screen readers announce them without interrupting the current context.
Navigation components: menus, breadcrumbs, pagination
Navigation components have the highest accessibility surface area of any category. Get them wrong and keyboard-only users can't operate your app.
Dropdown menus need role="menu", role="menuitem", arrow key navigation, and proper aria-expanded state. The hover-open pattern is an anti-pattern for accessibility — always pair it with a click/Enter trigger. Libraries like Radix UI and Headless UI handle this correctly out of the box, so unless you have a specific reason to roll your own, don't.
Breadcrumbs should use <nav aria-label="Breadcrumb"> with an ordered list. The current page item gets aria-current="page". Don't use > as a separator in the HTML — put it in CSS with ::after so screen readers don't read it.
Pagination is often over-engineered. For most apps, you need: previous button, next button, current page indicator, and maybe page number buttons for < 10 pages. Infinite scroll is not always better — it breaks the browser's back button behavior and makes footer links unreachable.
Marquee / ticker components (scrolling text or logo strips) are a different use case entirely. They're purely presentational and should have aria-hidden="true" on the animated element, with a static visually-hidden equivalent if the content is meaningful. The marquee component for React article covers the CSS animation approach vs. JavaScript-driven scroll for these.
Form components: inputs, selects, and validation
Form components are where component libraries earn their keep or reveal their shortcuts. Every input needs a visible label (not just a placeholder), an error message slot, and proper aria-describedby linking the error to the input.
The compound component pattern works well for form fields. A <Field> component wraps a <Label>, an <Input>, and an <ErrorMessage>, automatically wiring the id / htmlFor / aria-describedby relationships so you don't have to.
// Field.tsx — compound component for form fields
import { createContext, useContext, useId, ReactNode } from 'react';
const FieldContext = createContext<{ id: string; errorId: string } | null>(null);
export function Field({ children }: { children: ReactNode }) {
const id = useId();
return (
<FieldContext.Provider value={{ id, errorId: `${id}-error` }}>
<div className="flex flex-col gap-1.5">{children}</div>
</FieldContext.Provider>
);
}
export function FieldLabel({ children }: { children: ReactNode }) {
const { id } = useContext(FieldContext)!;
return <label htmlFor={id} className="text-sm font-medium text-neutral-200">{children}</label>;
}
export function FieldInput({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
const { id, errorId } = useContext(FieldContext)!;
return (
<input
id={id}
aria-describedby={errorId}
className="rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
{...props}
/>
);
}
export function FieldError({ message }: { message?: string }) {
const { errorId } = useContext(FieldContext)!;
if (!message) return null;
return <p id={errorId} role="alert" className="text-xs text-red-400">{message}</p>;
}For select components, the native <select> is still the most accessible option on mobile. Custom selects built with <div> and ARIA are notoriously hard to get right and often break on mobile screen readers. Only build a custom select when you need features the native element can't provide (custom option rendering, multi-select with checkboxes, etc.).
Validation feedback should appear after blur (not while typing — that's annoying) or on form submit. Use React Hook Form or Zod + React Hook Form for anything beyond trivial forms. Don't re-invent form state management.
Display components: tables, lists, grids
Data tables are among the most complex components to build correctly. Sortable columns, row selection, sticky headers, responsive collapsing — each adds meaningful complexity. Before building, honestly assess: does TanStack Table cover your needs? It handles virtualization, sorting, filtering, and row selection with a headless API. You bring the HTML and styles.
For grid layouts — feature grids, stat grids, dashboard panels — the Bento Grid pattern has become the dominant approach. It uses CSS Grid with grid-column: span N to create asymmetric, magazine-style layouts that still work on mobile with a single grid-cols-1 override. The bento grid component covers the exact implementation.
Lists seem simple until you need: virtualization (10,000+ items), drag-to-reorder, expandable rows, or nested trees. For virtualized lists, @tanstack/react-virtual is the right call — don't render 5,000 DOM nodes when 20 are visible.
What about icon systems? They connect directly to list and table components since icons appear in row actions, status badges, and column headers. A proper icon system for React should tree-shake unused icons, support consistent sizing via a prop, and never require a global icon font.
Skeleton loaders belong here too. They should match the exact shape of the loaded content — same width, height, and spacing. Use animate-pulse from Tailwind v4.0.2 for the shimmer effect. Don't use a generic spinner where a skeleton would give the user a better mental model of what's loading.
Motion and carousel components
Animation without purpose is noise. Every motion in your UI should communicate something: a button press confirms the action, a skeleton swap confirms data loaded, a panel slide confirms navigation. If you can't articulate what the animation communicates, remove it.
That said, some animations are purely delightful and that's valid — sparingly. The rule: never animate something that blocks the user from proceeding. Progress indicators are fine. Mandatory intro animations are not.
Carousels are divisive, but they're still useful for: product image galleries, testimonial rotators, and onboarding steps. The trap is building them with JavaScript-driven scroll when CSS scroll snap handles it natively. The React carousel component article shows both approaches, with the CSS-native version clocking in at roughly 60% less JavaScript.
For background animations — particle fields, gradient meshes, aurora effects — the performance math changes fast. Each canvas-based particle animation runs on the main thread unless you offload to a worker. Libraries like Three.js are powerful but carry a significant bundle cost. If you're considering adding 3D to your UI, read Three.js with React intro before committing to the dependency.
The prefers-reduced-motion media query is not optional. Wrap all non-essential animations: @media (prefers-reduced-motion: reduce) { .animated { animation: none; transition: none; } }. Approximately 25% of users in accessibility surveys have enabled this setting.
For particles background in React, the same reduced-motion rule applies — but there you also need to think about battery drain on mobile. A canvas animation running at 60fps on a phone is a real concern for users on limited battery.
Theme, tokens, and design system integration
A component library without a token system is just a collection of one-off styles. Tokens are named values — --color-surface-elevated, --spacing-4, --radius-card — that create a shared vocabulary between design and code.
With Tailwind v4.0.2, tokens live in your CSS via @theme blocks. You can reference them in both utility classes and arbitrary values: bg-[--color-surface-elevated]. This bridges the gap between the design tool (where tokens are defined) and the component (where they're consumed).
Theme toggle (light/dark/system) is a requirement, not a nice-to-have. The implementation is trickier than it looks because you need to: persist the preference in localStorage, sync it with the OS preference via prefers-color-scheme, and avoid a flash of the wrong theme on load. The theme toggle in React article covers the server-rendering edge cases that most tutorials skip.
CSS Modules vs. Tailwind is a real decision that affects your entire component library architecture. Short version: Tailwind wins for component libraries that need to be consumed across many projects with no build-step coordination. CSS Modules win for applications with complex, context-specific styling. The nuanced breakdown is in Tailwind vs CSS Modules.
One pattern that pays dividends: define your animation durations as tokens. --duration-fast: 150ms, --duration-base: 250ms, --duration-slow: 400ms. When a designer asks "can we make everything feel snappier", you change one value. Without tokens, you hunt through 200 CSS rules.
Compound components and render prop patterns
How you architect component APIs determines whether they're a joy or a burden to use six months later. Two patterns have stood the test of time: compound components (seen in the Form Field example above) and render props.
Compound components work by sharing implicit state through React Context. The parent component owns the state; child components read it without explicit prop threading. This is how <Select> / <Select.Option> should work, how <Tabs> / <Tabs.Panel> should work, how <Accordion> / <Accordion.Item> should work.
Render props let you invert control. Instead of the component deciding what to render, it hands rendering responsibility to the caller. Useful when the shape of the content is unpredictable.
// Render prop example: DataFetcher
interface DataFetcherProps<T> {
url: string;
children: (state: {
data: T | null;
loading: boolean;
error: Error | null;
}) => React.ReactNode;
}
export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [state, setState] = React.useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({ data: null, loading: true, error: null });
React.useEffect(() => {
fetch(url)
.then(r => r.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, [url]);
return <>{children(state)}</>;
}
// Usage
<DataFetcher<User[]> url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Skeleton />;
if (error) return <ErrorState message={error.message} />;
return <UserTable rows={data!} />;
}}
</DataFetcher>The render prop pattern has largely been superseded by custom hooks for logic-only concerns (useFetch, useScroll, useIntersection). But it's still the right tool when the component needs to provide DOM structure alongside the state — like a virtualized list provider that renders the scroll container.
Choosing the right framework: headless, styled, or full-stack
The React UI component ecosystem has split into three clear categories, and understanding the difference will save you weeks of wrong-direction work.
Headless libraries (Radix UI, Headless UI, Ark UI) provide behavior and accessibility with zero styles. You own 100% of the visual output. Maximum flexibility, maximum work. Best for product teams with a dedicated design system.
Styled libraries (shadcn/ui, Chakra UI, Mantine) give you styled defaults you can override. Faster to ship, but you're fighting the defaults as your design diverges. Good for startups moving fast.
Full-stack component libraries like Empire UI sit in an interesting position: they ship opinionated, production-ready visual components that work out of the box on a dark UI aesthetic, but expose the full source code so you can change anything. No fighting with overrides — you own the file. It's closer to "copy/paste with curation" than "install and configure".
For a head-to-head comparison of the major options, best free UI frameworks for React in 2026 does the honest comparison including bundle sizes, TypeScript support, and accessibility audit results.
What framework you choose also affects your testing strategy. Headless components are easiest to test because behavior is isolated from presentation. Styled components need visual regression tests. Empire UI components work well with both unit tests (React Testing Library) and visual tests (Playwright screenshots).
The short answer to which to pick: if you're building a product UI, start with a styled library and switch to headless when you feel constrained. If you're building a component library for others to use, start headless. If you need to ship a landing page in a week, use Empire UI.
FAQ
A component library is the code: React components you import and use. A design system is the full system: tokens, documentation, patterns, governance, and the library as one artifact of it. You can have a library without a design system, but a design system without a library is mostly theory.
Yes. Not because TypeScript is trendy — because component props are exactly the kind of interface where type errors are caught at compile time instead of runtime. Prop type mismatches are the most common source of bugs in component libraries. TypeScript with strict mode enabled eliminates most of them before you ever run the app.
Use the ARIA Authoring Practices Guide (APG) from W3C — it documents the exact keyboard behavior and ARIA roles for every component pattern. For focus management, the native <dialog> element handles focus trapping automatically. For everything else, @radix-ui/react-focus-scope is a solid standalone package.
Stick to transform and opacity — they're the only CSS properties that don't trigger layout or paint, so the browser can run them entirely on the GPU. Avoid animating width, height, top, left, or margin. Use Framer Motion's layout prop for animated layout changes, which internally uses FLIP (First Last Invert Play) to keep everything on the transform path.
Start with the 20 you actually use. Atoms: button, input, badge, avatar, icon, spinner, skeleton, tooltip. Molecules: card, modal, dropdown, tabs, form field, toast. Organisms: navbar, sidebar, data table, pagination. Add more only when two separate teams independently request the same component.
It depends. If the modal contains a form, keep it mounted (hidden) so the form state isn't lost when the user accidentally closes and reopens. If the modal fetches data on mount, you might want it to remount each open to get fresh data. There's no universal answer — expose a keepMounted prop and let the consumer decide.
Define your tokens in the @theme block in your global CSS file. Tailwind v4.0.2 automatically generates utility classes for @theme entries. For runtime-dynamic values (like a color picker output), write them directly as inline style style={{ '--color': value }} and reference them in Tailwind's arbitrary value syntax: bg-[--color].
Reach for a headless library (Radix, Headless UI) when the component has complex interactive behavior: comboboxes, date pickers, context menus, dialogs. These patterns have subtle accessibility requirements that are easy to get wrong and painful to debug. Build from scratch when the component is purely visual with no interactive state — a badge, a stat card, a divider.