React: UI Component Library

Last updated: 2026-08-26

Tom started from scratch styling the admin panel for his blog. Six months later, the code was riddled with repetitive style={{}} inline styles and hand-coded CSS classe names. Development of new features was slowing down, and the UI styles were inconsistent across different pages. He knew it was time to adopt a professional UI component library, but faced with numerous options—such as Ant Design, Material UI, and Headless UI—Tom was at a loss.


1. What You'll Learn



2. Conceptual Diagrams

Tom outlined the decision-making process for selecting a UI component library: based on three factors—project type, team background, and customization needs—he selected the most suitable component library solution. Different types of component libraries have their own advantages and limitations in various scenarios.

The decision tree first branches by project type: back-end and middle-tier projects prioritize component completeness, while front-end projects prioritize visual effects and design guidelines. Team background directly impacts the learning curve and development efficiency: React teams get up to speed faster with Ant Design, while design-driven teams find it smoother to use Material UI in conjunction with Figma design guidelines. The final decision point is on-demand loading and performance optimization—regardless of which library is chosen, proper on-demand loading configuration must be set up when integrating it into Next.js.

100%
flowchart TD
    A[Select UI Component Library] --> B{Project Type?}
    B -->|Backend Management/Enterprise Applications| C[Ant Design<br/>Comprehensive enterprise-level components]
    B -->|Consumer-Facing Front End| D[Material UI<br/>Modern Visual Style]
    B -->|Highly Customized Requirements| E[Headless UI<br/>No style + Customizable]
    C --> F{Team Background?}
    D --> F
    E --> F
    F -->|React Team| G[Ant Design<br/>React Best Ecology]
    F -->|Designer-led| H[Material UI<br/>Mature design specifications]
    F -->|Fully Customizable Design| I[Headless UI<br/>Full Control Over Styles]
    G --> J[Integrated into Next.js]
    H --> J
    I --> J
    J --> K[Load on Demand Tree Shaking]
    K --> L[Theme Customization]
    L --> M[Performance Optimization]


3. A Real-Life Scenario

Tom’s admin dashboard was initially styled entirely by hand. Six months later, the project had expanded to more than 30 pages, each with its own CSS file. The button styles were inconsistent, the table components lacked functionality, and he had implemented the date picker from scratch three times. He spent a week researching mainstream UI component libraries.

In the end, Tom chose Ant Design for three reasons: First, Ant Design offers the most comprehensive set of components—enterprise-grade components such as tables, forms, and date pickers are ready to use right out of the box; second, it supports on-demand loading and tree shaking, so it doesn’t bloat the project; third, the Next.js integration is already well-established, and the community has a wealth of practical examples. He also used Headless UI to customize some special components not available in Ant Design.

Tom’s selection process is very informative: he first spent a week integrating three libraries into a test project and evaluated them based on three criteria—development efficiency, performance impact, and learning curve. Ant Design came out on top across the board in an enterprise backend scenario. This “test first, then decide” approach can also be applied to your own project selection process.

(1) Comparison of Component Library Options

for Comparison Ant Design Material UI Headless UI
Design Language Enterprise-Level Back-End Google Material Design No Styling (Behavioral Logic)
Number of components 60+ 50+ ~15
Theme Customization ConfigProvider + token ThemeProvider + sx prop Fully Customizable (Tailwind/CSS)
Styling Options CSS-in-JS (v5) CSS-in-JS (Emotion) No Built-in Styles
Internationalization 50+ built-in languages 30+ built-in languages None built-in
Use Cases Backend Management, Enterprise Applications Consumer-Facing Applications Highly Customized Projects
Tree Shaking ✅ Automatic ✅ Automatic ✅ Automatic
Learning Curve Moderate Moderate Low (requires custom styling)

When selecting a UI component library, you should consider three key factors: component comprehensiveness (whether it covers your business needs), customization flexibility (whether it meets designers’ requirements), and performance impact (whether it slows down the first-screen load).

Ant Design (antd) is the most popular React UI library in China, open-sourced by Ant Financial. Its design language is geared toward enterprise-level back-end and middle-tier applications, providing a full suite of components ranging from buttons and tables to date pickers and tree controls. The interaction logic between components has been extensively tested, making them ready for use in complex scenarios—such as pagination, sorting, and filter synchronization in data tables.

Material UI (MUI) is a React implementation of Google’s Material Design guidelines. Its strengths lie in its mature design guidelines, modern visual style, and robust theme system. It is well-suited for consumer-facing front-end applications and designer-led teams.

Headless UI differs from the two categories above—it does not provide any predefined styles, but only provides behavioral logic (accessibility, keyboard navigation, focus management). Developers are entirely responsible for implementing the styling themselves using Tailwind CSS or CSS Modules. It is suitable for projects that require a highly customized visual style.

▶ Example 1: Comparison of Button Implementations Across Component Libraries

Output:

TEXT 📖 Display only
Side-by-side comparison: HTML syntax vs JSX syntax showing equivalent markup. UI library component with pre-built styling and behavior
TSX
// === Ant Design button ===
// Ready to Use Out of the Box,Built-in Styles,Through type Switch Visual Styles by Attribute
import { Button, Space } from 'antd'

function AntDButtons() {
  return (
    <Space wrap>
      <Button type="primary">Main Buttons</Button>
      <Button>Default Button</Button>
      <Button type="dashed">Dotted-line button</Button>
      <Button type="link">Link Button</Button>
      <Button type="primary" danger>Danger Button</Button>
      <Button loading>Loading...</Button>
      <Button type="primary" icon={<SearchOutlined />}>Search</Button>
    </Space>
  )
}

// === Material UI Button ===
// Through variant and  color Combining Properties to Achieve Different Styles
import Button from '@mui/material/Button'
import Stack from '@mui/material/Stack'
import SaveIcon from '@mui/icons-material/Save'

function MUIButtons() {
  return (
    <Stack direction="row" spacing={2}>
      <Button variant="contained">Fill Button</Button>
      <Button variant="outlined">Outline Button</Button>
      <Button variant="text">Text Button</Button>
      <Button variant="contained" color="error">Danger</Button>
      <Button variant="contained" disabled>Disable</Button>
      <Button variant="contained" startIcon={<SaveIcon />}>Save</Button>
    </Stack>
  )
}

// === Headless UI + Tailwind ===
// Unstyled Components + Custom Class
import { Button as HeadlessButton } from '@headlessui/react'

function HeadlessButtons() {
  return (
    <div className="flex gap-2">
      <HeadlessButton className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-500 data-[active]:bg-blue-700">
        Main Buttons
      </HeadlessButton>
      <HeadlessButton className="rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50">
        Default Button
      </HeadlessButton>
      <HeadlessButton className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-500">
        Danger Button
      </HeadlessButton>
    </div>
  )
}

Output:

TEXT 📖 Display only
Tailwind: className="flex gap-4 p-6 rounded-lg shadow" → utility classes compose inline. No custom CSS needed.

(2) Integrating Ant Design with Next.js

When integrating Ant Design into Next.js, several key issues must be addressed: CSS-in-JS compatibility, on-demand loading, and client component boundaries. Ant Design v5 uses the CSS-in-JS (cssinjs) approach, which requires configuration in Next.js to ensure styles are injected correctly.

Integration Issues Cause Solution
CSS-in-JS SSR Compatibility CSS-in-JS Styles Cannot Be Injected on the Server Side Wrap Client Component Boundaries with AntdProvider
First-screen size too large The entire antd library is bundled into the initial JS file dynamic() Dynamic import + optimizePackageImports
Server Component Error antd components depend on browser APIs Add 'use client' at the top of the file using the component
Flash of Unstyled Content (FOUC) No styles on the server side; delayed injection on the client side Extract critical CSS or use the App component for centralized management
Theme switching flickers Theme token is loaded only on the client side Set the default theme in ConfigProvider and inject it via SSR

To optimize performance, components should use dynamic imports (dynamic) for on-demand loading to avoid bundling the entire component library into the above-the-fold JavaScript. Ant Design’s Tree Shaking takes effect automatically when building with ES Modules—simply import directly from antd; no configuration of babel-plugin-import is required.

▶ Example 2: Full Integration of Ant Design and Next.js

Output:

TEXT 📖 Display only
Displays: "Main Buttons"
TSX
// app/providers.tsx - Ant Design Themes and Configuration Provider
'use client'
import { useState } from 'react'
import { ConfigProvider, theme, App } from 'antd'
import zhCN from 'antd/locale/zh_CN'

export function AntdProvider({ children }: { children: React.ReactNode }) {
  const [isDark] = useState(false)

  return (
    <ConfigProvider
      locale={zhCN}
      theme={{
        // Switch to Light Mode/Dark Theme
        algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
        // Custom Theme Colors
        token: {
          colorPrimary: '#1677ff',
          borderRadius: 6,
          colorBgContainer: isDark ? '#141414' : '#ffffff',
        },
      }}
    >
      <App>{children}</App>
    </ConfigProvider>
  )
}

// app/layout.tsx - Global Import AntdProvider
import { AntdProvider } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        <AntdProvider>{children}</AntdProvider>
      </body>
    </html>
  )
}

// app/dashboard/page.tsx - Use dynamic import to load on demand Ant Design Components
// Avoid putting the entire antd Pack into the first screen JS in 
import dynamic from 'next/dynamic'

// Dynamic Import(Load only when needed)
const DataTable = dynamic(() => import('@/components/DataTable'), {
  loading: () => <div>Loading the table......</div>,
})

const DatePicker = dynamic(() => import('antd').then(mod => mod.DatePicker), {
  loading: () => <div>Loading the date picker......</div>,
})

async function DashboardPage() {
  // Analog Data Acquisition
  const data = await fetch('https://api.example.com/table-data').then(r => r.json())

  return (
    <div style={{ padding: 24 }}>
      <h1>Management Dashboard</h1>
      <div style={{ marginBottom: 16 }}>
        <DatePicker />
      </div>
      <DataTable data={data} />
    </div>
  )
}
export default DashboardPage

// components/DataTable.tsx - Complete Table Component
'use client'
import { Table, Tag, Space, Button, Popconfirm } from 'antd'
import type { ColumnsType } from 'antd/es/table'

interface UserData {
  key: number
  name: string
  age: number
  address: string
  status: 'active' | 'inactive'
}

function DataTable({ data }: { data: UserData[] }) {
  const columns: ColumnsType<UserData> = [
    { title: 'Name', dataIndex: 'name', key: 'name', sorter: (a, b) => a.name.localeCompare(b.name) },
    { title: 'Age', dataIndex: 'age', key: 'age', sorter: (a, b) => a.age - b.age },
    { title: 'Address', dataIndex: 'address', key: 'address' },
    {
      title: 'Status',
      dataIndex: 'status',
      key: 'status',
      render: (status: string) => (
        <Tag color={status === 'active' ? 'green' : 'red'}>
          {status === 'active' ? 'Enable' : 'Disable'}
        </Tag>
      ),
    },
    {
      title: 'Operation',
      key: 'action',
      render: (_: any, record: UserData) => (
        <Space>
          <Button type="link" onClick={() => console.log('Edit', record.key)}>Edit</Button>
          <Popconfirm title="Confirm Deletion?" onConfirm={() => console.log('Delete', record.key)}>
            <Button type="link" danger>Delete</Button>
          </Popconfirm>
        </Space>
      ),
    },
  ]

  return (
    <Table
      columns={columns}
      dataSource={data}
      pagination={{ pageSize: 10, showSizeChanger: true, showTotal: (total) => `Total ${total} items` }}
      bordered
      size="middle"
    />
  )
}
export default DataTable

Output:

TEXT 📖 Display only
State: selected, query

(3) Headless UI + Tailwind CSS customization

Headless UI offers a completely different approach to component libraries—style-free components. It exposes behavioral logic (such as closing an open dialog by pressing the Escape key, keyboard navigation for combo boxes, and animation management for transitions), while leaving styling entirely up to the developer. This means your UI can perfectly match the design mockups without needing to override third-party default styles.

Tom encountered some scenarios that Ant Design didn’t cover: a custom filter panel that required specific interactions, and a drop-down suggestion box for search results that needed special styling. These can be implemented very flexibly using Headless UI. Combined with Tailwind CSS utility classes, you can build a UI that matches the design mockups without writing a single line of custom CSS.

Headless UI Components Provided Behavior Logic Parts Requiring Customization
Dialog Escape: Close, Lock Focus, Disable Background Scrolling Appearance, Animation, Mask Style
Combo Box Keyboard Navigation, Search and Filter, Option Highlighting Input Fields, Drop-down Panels, Option Styles
Menu Navigate with arrow keys; click outside to close Menu items, icons, and divider styles
Switch Toggle State, Accessibility (ARIA) Switch Track, Slider Style
Tab Group Arrow Key Navigation, Panel Synchronization Tab and Panel Layout Styles
Transition Fade-In/Fade-Out Animation Management Animation CSS Class Names

▶ Example 3: Custom Headless UI Components

Output:

TEXT 📖 Display only
... ('Edit', record.key)
... ('Delete', record.key)
TSX
// components/SearchCombobox.tsx
// Usage Headless UI 's  Combobox Implementing a search suggestion drop-down menu
'use client'
import { useState } from 'react'
import {
  Combobox,
  ComboboxInput,
  ComboboxButton,
  ComboboxOptions,
  ComboboxOption,
  Transition,
} from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'

const people = [
  { id: 1, name: 'Alice', role: 'Admin' },
  { id: 2, name: 'Bob', role: 'Editor' },
  { id: 3, name: 'Charlie', role: 'Author' },
  { id: 4, name: 'Diana', role: 'Reader' },
  { id: 5, name: 'Eve', role: 'Admin' },
]

function SearchCombobox() {
  const [selected, setSelected] = useState(people[0])
  const [query, setQuery] = useState('')

  const filtered = query === ''
    ? people
    : people.filter((person) =>
        person.name.toLowerCase().includes(query.toLowerCase())
      )

  return (
    <div className="w-72">
      <Combobox value={selected} onChange={setSelected}>
        <div className="relative">
          <ComboboxInput
            className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
            displayValue={(person: any) => person?.name}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search Users..."
          />
          <ComboboxButton className="absolute inset-y-0 right-0 flex items-center pr-2">
            <ChevronDownIcon className="h-5 w-5 text-gray-400" />
          </ComboboxButton>
        </div>

        <Transition
          enter="transition duration-100 ease-out"
          enterFrom="transform scale-95 opacity-0"
          enterTo="transform scale-100 opacity-100"
          leave="transition duration-75 ease-out"
          leaveFrom="transform scale-100 opacity-100"
          leaveTo="transform scale-95 opacity-0"
        >
          <ComboboxOptions className="absolute z-10 mt-1 max-h-60 w-72 overflow-auto rounded-lg bg-white py-1 shadow-lg ring-1 ring-black/5">
            {filtered.length === 0 && query !== '' ? (
              <div className="px-3 py-2 text-sm text-gray-500">No matching results found</div>
            ) : (
              filtered.map((person) => (
                <ComboboxOption
                  key={person.id}
                  value={person}
                  className="cursor-pointer px-3 py-2 text-sm data-[focus]:bg-blue-100 data-[selected]:bg-blue-50"
                >
                  {({ selected }) => (
                    <div className="flex justify-between">
                      <span className={selected ? 'font-medium' : ''}>{person.name}</span>
                      <span className="text-gray-400">{person.role}</span>
                    </div>
                  )}
                </ComboboxOption>
              ))
            )}
          </ComboboxOptions>
        </Transition>
      </Combobox>

      {selected && (
        <p className="mt-2 text-sm text-gray-600">
          Selected:{selected.name}({selected.role})
        </p>
      )}
    </div>
  )
}
export default SearchCombobox

Output:

TEXT 📖 Display only
Unknown URL → fallback "404: Page Not Found" route. Catch-all path="*" renders Not Found component.

▶ Example 4: Performance Optimization Configuration in next.config.ts

Output:

TEXT 📖 Display only
Displays: "person.name.toLowerCase().includes(query.toLowerCase())     ". State: selected (setter: setSelected), query (setter: setQuery)
TSX
// next.config.ts - Performance Optimization Configuration for the Component Library
import type { NextConfig } from 'next'

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

const nextConfig: NextConfig = withBundleAnalyzer({
  optimizePackageImports: [
    'antd',
    '@ant-design/icons',
    '@mui/material',
    '@mui/icons-material',
    '@headlessui/react',
  ],
  transpilePackages: ['antd'],
  experimental: {
    optimizeCss: true,
  },
})

export default nextConfig

// package.json scripts:
// "analyze": "ANALYZE=true next build"
// "analyze:server": "ANALYZE=true BUNDLE_ANALYZE=server next build"
// "analyze:browser": "ANALYZE=true BUNDLE_ANALYZE=browser next build"

Output:

TEXT 📖 Display only
next.config.js optimization: images.domains, react.strictMode, compiler.removeConsole, modularizeImports for tree-shaking

▶ Example 5: Strategies for Mixing and Matching Multiple Component Libraries

Output:

TEXT 📖 Display only
UI library component with pre-built styling and behavior
TSX
// components/HybridTable.tsx - Ant Design Table + Headless UI Custom Filters
'use client'
import { useState } from 'react'
import { Table, Tag } from 'antd'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import type { ColumnsType } from 'antd/es/table'

interface Product {
  key: string
  name: string
  price: number
  category: string
  status: 'in_stock' | 'out_of_stock'
}

const products: Product[] = [
  { key: '1', name: 'Laptop', price: 999, category: 'Electronics', status: 'in_stock' },
  { key: '2', name: 'Desk Chair', price: 299, category: 'Furniture', status: 'out_of_stock' },
  { key: '3', name: 'Coffee Maker', price: 79, category: 'Kitchen', status: 'in_stock' },
]

function HybridTable() {
  const [categoryFilter, setCategoryFilter] = useState<string>('all')

  const filtered = categoryFilter === 'all'
    ? products
    : products.filter(p => p.category === categoryFilter)

  const columns: ColumnsType<Product> = [
    { title: 'Name', dataIndex: 'name', key: 'name' },
    { title: 'Price', dataIndex: 'price', key: 'price', render: (v: number) => `$${v}` },
    { title: 'Category', dataIndex: 'category', key: 'category' },
    {
      title: 'Status',
      dataIndex: 'status',
      key: 'status',
      render: (s: string) => (
        <Tag color={s === 'in_stock' ? 'green' : 'red'}>
          {s === 'in_stock' ? 'In Stock' : 'Out of Stock'}
        </Tag>
      ),
    },
  ]

  return (
    <div>
      <div className="mb-4 flex items-center gap-2">
        <Popover className="relative">
          <PopoverButton className="rounded border px-3 py-1.5 text-sm">
            Filter: {categoryFilter === 'all' ? 'All' : categoryFilter}
          </PopoverButton>
          <PopoverPanel className="absolute z-10 mt-1 w-40 rounded bg-white py-1 shadow-lg">
            {['all', 'Electronics', 'Furniture', 'Kitchen'].map(cat => (
              <button
                key={cat}
                className="block w-full px-3 py-1.5 text-left text-sm hover:bg-blue-50"
                onClick={() => setCategoryFilter(cat)}
              >
                {cat === 'all' ? 'All Categories' : cat}
              </button>
            ))}
          </PopoverPanel>
        </Popover>
      </div>
      <Table columns={columns} dataSource={filtered} pagination={false} size="small" />
    </div>
  )
}
export default HybridTable

Output:

TEXT 📖 Display only
Tailwind: className="flex gap-4 p-6 rounded-lg shadow" → utility classes compose inline. No custom CSS needed.

❓ FAQ

Q How do I choose between Ant Design, Material UI, and Headless UI?
A Three principles for selection: Consider the project type—use Ant Design for enterprise backends and Material UI for consumer-facing applications; consider customization needs—use Headless UI + Tailwind for high levels of customization; and consider team experience—React teams tend to find Ant Design more intuitive. You can also use a hybrid approach: use Ant Design as the base component library and Headless UI to supplement with custom components.
Q What should I keep in mind when using Ant Design in Next.js?
A There are three key points to note: First, Ant Design components must be used within Client Components (with 'use client'); they cannot be used directly in Server Components; Second, use dynamic to dynamically import components to avoid bundling the entire antd library into the above-the-fold JavaScript; third, configure themes and internationalization in ConfigProvider to ensure global style consistency. Ant Design v5 no longer requires babel-plugin-import, as Tree Shaking is automatically enabled.
Q Will a component library make a project bloated?
A It certainly will if used improperly. Optimization methods: Use dynamic to load components on demand (dynamic(() => import('antd').then(m => m.Button))); configure next.config.ts’s optimizePackageImports; import only the components you need (import { Button } from 'antd' instead of importing everything); analyze the bundle size (@next/bundle-analyzer) to identify abnormal dependencies. After optimization, the impact of integrating Ant Design on the first-screen JavaScript is typically no more than 30KB.
Q When customizing components, should I modify the component library’s theme or write my own CSS?
A Prioritize modifying the component library’s theme—both Ant Design’s ConfigProvider and Material UI’s ThemeProvider provide comprehensive theme systems that allow you to override tokens such as colors, spacing, rounded corners, and fonts. Use CSS Modules or Tailwind to supplement styles that the component library’s theme system cannot cover. Avoid using !important to override component library styles—you should achieve this by using theme tokens or overriding CSS variables.
Q What are the differences between Ant Design v4 and v5?
A The biggest change in Ant Design v5 is the migration from Less to the CSS-in-JS (cssinjs) approach, eliminating the need for the Less compiler and babel-plugin-import. Additionally, v5 introduces the App component (which centrally manages static methods such as message, notification, and modal), the theme object (for fine-grained token customization), and improved SSR support. The TypeScript type definitions in v5 are also more comprehensive. If you’re upgrading from v4, be sure to pay attention to CSS-in-JS compatibility and changes to the theme API.

📖 Summary


📝 Exercises

  1. Integrate Ant Design into the blog’s admin interface: Create a providers.tsx theme configured for Ant Design and internationalization (set to Chinese), and wrap it in the global layout AntdProvider. Use components such as Button, Table, Tag, and Space to redesign the blog’s user management page in the admin interface.
  2. Use the Combobox component from Headless UI + Tailwind CSS to implement a user search selector: retrieve a list of users from the API, support keyword search and filtering, and display user information when a user is selected. Compare this with Ant Design’s Select component to experience the level of customization flexibility offered by Headless UI.
  3. Optimize the component library’s performance for the project: Use @next/bundle-analyzer to analyze changes in package size before and after integrating Ant Design. Switch at least three large components (DatePicker, Table, TreeSelect) to dynamic loading using dynamic. Add optimizePackageImports: ['antd', '@ant-design/icons'] to next.config.ts and compare the first-screen JavaScript size and Lighthouse performance scores before and after optimization.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏