Eine React-App von Grund auf zu stylen ist zeitaufwendig. Buttons, Formulare, Modals, Dropdowns – alles muss designed, entwickelt und getestet werden. UI-Bibliotheken lösen dieses Problem: Sie liefern fertige, getestete Komponenten mit konsistentem Design. Der Trade-off: Man kauft Produktivität mit Flexibilitätsverlust und Bundle-Size.
Das Spektrum reicht von CSS-Frameworks wie Bootstrap (bringt Styles, React-Bindings optional) über vollständige Component-Libraries wie Material UI (React-first, opinionated Design) bis zu Headless-Libraries wie Radix UI (Logik ohne Styles, maximale Freiheit).
CSS-Frameworks (Bootstrap, Tailwind CSS) - Liefern: Utility-Classes oder vordefinierte Klassen - Integration: CSS-Datei einbinden - React-Komponenten: Optional via Wrapper-Libraries - Flexibilität: Hoch (nur CSS, eigene Komponenten)
Component Libraries (Material UI, Ant Design, Chakra UI) - Liefern: Fertige React-Komponenten mit Styling - Integration: npm-Paket, importiere Komponenten - Design: Opinionated (Material Design, Ant Design System) - Flexibilität: Mittel (Theme-System, aber Design-Sprache fix)
Headless Libraries (Radix UI, Headless UI, React Aria) - Liefern: Logik, Accessibility, keine Styles - Integration: npm-Paket, eigenes CSS/Tailwind - Design: Agnostic - Flexibilität: Maximal (komplette Style-Kontrolle)
Bootstrap ist primär ein CSS-Framework. Für React gibt es
react-bootstrap – React-Komponenten, die Bootstrap-Styles
nutzen.
Option 1: CDN (schnell für Prototypen)
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<!-- Bootstrap CSS via CDN -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
// Mit nativen Bootstrap-Klassen
function App() {
return (
<div className="container">
<div className="row">
<div className="col-md-6">
<button className="btn btn-primary">Click me</button>
</div>
</div>
</div>
);
}
Nachteile: - JavaScript-Komponenten (Modals, Dropdowns) brauchen Bootstrap JS + jQuery - Keine TypeScript-Typen - Kein React-Lifecycle-Integration
Option 2: npm + react-bootstrap (empfohlen)
npm install react-bootstrap bootstrap// main.tsx
import 'bootstrap/dist/css/bootstrap.min.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// App.tsx
import { Container, Row, Col, Button, Card, Modal } from 'react-bootstrap';
import { useState } from 'react';
function App() {
const [show, setShow] = useState(false);
return (
<Container>
<Row className="mt-4">
<Col md={6}>
<Card>
<Card.Body>
<Card.Title>React Bootstrap Example</Card.Title>
<Card.Text>
Using Bootstrap components as React components.
</Card.Text>
<Button variant="primary" onClick={() => setShow(true)}>
Open Modal
</Button>
</Card.Body>
</Card>
</Col>
</Row>
<Modal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton>
<Modal.Title>Modal Title</Modal.Title>
</Modal.Header>
<Modal.Body>Modal content goes here</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShow(false)}>
Close
</Button>
<Button variant="primary">
Save Changes
</Button>
</Modal.Footer>
</Modal>
</Container>
);
}
Vorteile: - React-native Komponenten - TypeScript-Support - Kein jQuery - Event-Handler sind React-Events
Bootstrap nutzt CSS-Variablen für Theming:
// src/styles/custom.scss
// Bootstrap-Variablen überschreiben
$primary: #5e35b1;
$secondary: #26a69a;
$font-family-sans-serif: 'Inter', system-ui, sans-serif;
@import 'bootstrap/scss/bootstrap';// main.tsx
import './styles/custom.scss'; // Statt bootstrap.min.css
Oder zur Runtime mit CSS-Variablen:
/* src/index.css */
:root {
--bs-primary: #5e35b1;
--bs-secondary: #26a69a;
--bs-border-radius: 0.5rem;
}Material UI (MUI) implementiert Googles Material Design als React-Komponenten. Umfangreich, opinionated, produktionsreif.
npm install @mui/material @emotion/react @emotion/styledMUI nutzt Emotion für CSS-in-JS.
// App.tsx
import {
ThemeProvider,
createTheme,
CssBaseline,
Container,
Button,
TextField,
Card,
CardContent,
Typography
} from '@mui/material';
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
typography: {
fontFamily: 'Roboto, Arial, sans-serif',
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline /> {/* Normalisiert Browser-Styles */}
<Container maxWidth="md" sx={{ mt: 4 }}>
<Card>
<CardContent>
<Typography variant="h4" gutterBottom>
Material UI Example
</Typography>
<TextField
label="Email"
variant="outlined"
fullWidth
sx={{ mb: 2 }}
/>
<TextField
label="Password"
type="password"
variant="outlined"
fullWidth
sx={{ mb: 2 }}
/>
<Button variant="contained" color="primary" fullWidth>
Login
</Button>
</CardContent>
</Card>
</Container>
</ThemeProvider>
);
}
// theme.ts
import { createTheme } from '@mui/material';
export const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#5e35b1',
light: '#9162e4',
dark: '#280680',
},
secondary: {
main: '#26a69a',
light: '#64d8cb',
dark: '#00766c',
},
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontSize: '2.5rem',
fontWeight: 600,
},
button: {
textTransform: 'none', // Keine Uppercase-Buttons
},
},
shape: {
borderRadius: 8,
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
padding: '10px 24px',
},
},
defaultProps: {
disableElevation: true,
},
},
MuiCard: {
styleOverrides: {
root: {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
},
},
},
});
// App.tsx
import { ThemeProvider } from '@mui/material';
import { theme } from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
{/* App */}
</ThemeProvider>
);
}
MUI’s sx prop ermöglicht Theme-aware Inline-Styles:
import { Box, Typography } from '@mui/material';
function Component() {
return (
<Box
sx={{
bgcolor: 'primary.main', // Theme-Farbe
color: 'primary.contrastText', // Automatischer Kontrast
p: 2, // padding: theme.spacing(2)
mt: 4, // margin-top: theme.spacing(4)
borderRadius: 1, // theme.shape.borderRadius
'&:hover': {
bgcolor: 'primary.dark',
},
}}
>
<Typography variant="h6">Styled Box</Typography>
</Box>
);
}
| Shorthand | CSS Property | Theme-Wert |
|---|---|---|
p |
padding |
theme.spacing(n) |
m |
margin |
theme.spacing(n) |
bgcolor |
background-color |
theme.palette.* |
color |
color |
theme.palette.* |
import { Button, ButtonProps } from '@mui/material';
// Custom Button mit eigenen Props
interface CustomButtonProps extends ButtonProps {
loading?: boolean;
}
function CustomButton({ loading, children, ...props }: CustomButtonProps) {
return (
<Button disabled={loading} {...props}>
{loading ? 'Loading...' : children}
</Button>
);
}
PrimeReact bietet umfangreiche Komponenten für Business-Anwendungen: DataTables, Charts, Forms.
npm install primereact primeicons// main.tsx
import 'primereact/resources/themes/lara-light-indigo/theme.css';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';
// App.tsx
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Card } from 'primereact/card';
import { useState } from 'react';
interface Product {
id: number;
name: string;
price: number;
category: string;
}
function App() {
const [products] = useState<Product[]>([
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics' },
{ id: 2, name: 'Mouse', price: 29, category: 'Electronics' },
{ id: 3, name: 'Keyboard', price: 79, category: 'Electronics' },
]);
return (
<div className="p-4">
<Card title="PrimeReact Example" className="mb-4">
<div className="p-fluid">
<div className="p-field mb-3">
<label htmlFor="search">Search</label>
<InputText id="search" placeholder="Search products..." />
</div>
<Button label="Add Product" icon="pi pi-plus" className="p-button-success" />
</div>
</Card>
<Card title="Products">
<DataTable value={products} paginator rows={10} responsiveLayout="scroll">
<Column field="id" header="ID" sortable />
<Column field="name" header="Name" sortable />
<Column field="price" header="Price" sortable body={(data) => `$${data.price}`} />
<Column field="category" header="Category" sortable />
</DataTable>
</Card>
</div>
);
}
PrimeReact nutzt vorgefertigte Themes. Custom-Themes via CSS-Variablen:
/* src/theme.css */
:root {
--primary-color: #5e35b1;
--primary-color-text: #ffffff;
--surface-0: #ffffff;
--surface-50: #fafafa;
--surface-100: #f5f5f5;
--text-color: #212121;
--border-radius: 6px;
}Oder Designer-Tool nutzen: https://www.primefaces.org/designer/primereact
Chakra UI kombiniert Component Library mit Utility-Props wie Tailwind.
npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion// main.tsx
import { ChakraProvider } from '@chakra-ui/react';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</React.StrictMode>
);
// App.tsx
import {
Box,
Button,
Container,
Heading,
Input,
Stack,
Card,
CardBody,
Text
} from '@chakra-ui/react';
function App() {
return (
<Container maxW="container.md" py={8}>
<Card>
<CardBody>
<Heading mb={4}>Chakra UI Example</Heading>
<Stack spacing={3}>
<Input placeholder="Email" size="lg" />
<Input placeholder="Password" type="password" size="lg" />
<Button colorScheme="purple" size="lg">
Login
</Button>
</Stack>
</CardBody>
</Card>
{/* Utility Props wie Tailwind */}
<Box
mt={4}
p={4}
bg="purple.50"
borderRadius="md"
_hover={{ bg: 'purple.100' }}
>
<Text color="purple.800">
Hover me to see background change
</Text>
</Box>
</Container>
);
}
// theme.ts
import { extendTheme } from '@chakra-ui/react';
export const theme = extendTheme({
colors: {
brand: {
50: '#f5e9ff',
100: '#dbb8ff',
500: '#5e35b1',
900: '#280680',
},
},
fonts: {
heading: '"Inter", sans-serif',
body: '"Inter", sans-serif',
},
components: {
Button: {
baseStyle: {
fontWeight: 'semibold',
},
defaultProps: {
colorScheme: 'brand',
},
},
},
});
// main.tsx
import { ChakraProvider } from '@chakra-ui/react';
import { theme } from './theme';
ReactDOM.createRoot(document.getElementById('root')!).render(
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
);
Headless UI (von Tailwind Labs) liefert Logik und Accessibility, kein Styling.
npm install @headlessui/reactimport { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
function Example() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
onClick={() => setIsOpen(true)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Open Dialog
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={() => setIsOpen(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-900">
Payment successful
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Your payment has been successfully submitted.
</p>
</div>
<div className="mt-4">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200"
onClick={() => setIsOpen(false)}
>
Got it, thanks!
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
}
Headless UI übernimmt: - Focus-Management - Keyboard-Navigation - ARIA-Attribute - Open/Close-State - Animation-Transitions
Du schreibst: Alle Styles.
| Library | Bundle Size | Learning Curve | Design Freedom | Best For |
|---|---|---|---|---|
| Bootstrap | ~25KB CSS | Niedrig | Mittel | Quick Prototypes, interne Tools |
| Material UI | ~300KB | Mittel | Niedrig | Material Design Apps, Enterprise |
| PrimeReact | ~400KB | Mittel | Niedrig | Data-heavy Business Apps |
| Chakra UI | ~200KB | Niedrig | Hoch | Developer Experience, Rapid Dev |
| Ant Design | ~600KB | Hoch | Niedrig | Chinese Market, Admin Panels |
| Headless UI | ~20KB | Mittel | Maximal | Custom Designs, Tailwind Users |
| Radix UI | ~50KB | Hoch | Maximal | Design Systems, Full Control |
Problem: Globale CSS-Regeln verschiedener Libraries kollidieren.
// ❌ Falsch: Bootstrap + Material UI gleichzeitig
import 'bootstrap/dist/css/bootstrap.min.css';
import '@mui/material/styles';
// Buttons haben konkurrierende Styles
<button className="btn btn-primary">Bootstrap</button>
<Button variant="contained">Material UI</Button>
Lösungen:
1. CSS Modules (Scoping)
// Button.module.css
.button {
composes: btn btn-primary from 'bootstrap/dist/css/bootstrap.min.css';
/* Überschreibungen */
border-radius: 8px;
}
2. CSS-in-JS Libraries isolieren automatisch
Material UI (Emotion) und Chakra generieren unique Klassen-Namen → keine Kollisionen.
3. Prefix für globale Styles
// Prefix alle Bootstrap-Klassen
.bootstrap-scope {
@import 'bootstrap/scss/bootstrap';
}<div className="bootstrap-scope">
<button className="btn btn-primary">Scoped Bootstrap</button>
</div>
UI Libraries sind oft groß. Tree-Shaking hilft.
❌ Falsch: Default Import importiert alles
import * as MaterialUI from '@mui/material';
<MaterialUI.Button>Click</MaterialUI.Button>
Bundle enthält alle MUI-Komponenten, auch wenn nur Button genutzt wird.
✓ Richtig: Named Imports
import { Button, TextField } from '@mui/material';
<Button>Click</Button>
Webpack/Vite können ungenutzte Komponenten entfernen.
Noch besser: Direct Imports (wenn Tree-Shaking nicht funktioniert)
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
Lazy-load heavy Komponenten:
import { lazy, Suspense } from 'react';
// DataTable ist groß, nur laden wenn gebraucht
const DataTable = lazy(() => import('primereact/datatable').then(m => ({
default: m.DataTable
})));
function App() {
const [showTable, setShowTable] = useState(false);
return (
<div>
<button onClick={() => setShowTable(true)}>Show Table</button>
{showTable && (
<Suspense fallback={<div>Loading table...</div>}>
<DataTable value={data} />
</Suspense>
)}
</div>
);
}
Problem: App ist komplett abhängig von Material UI. Wechsel zu Chakra? Hunderte Komponenten müssen umgeschrieben werden.
Lösung: Abstraction Layer
// components/Button.tsx - Eigener Wrapper
import { Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', size = 'md', ...props }: ButtonProps) {
// Map eigene Props zu MUI-Props
const muiVariant = variant === 'primary' ? 'contained' :
variant === 'secondary' ? 'outlined' : 'text';
const muiSize = size === 'sm' ? 'small' :
size === 'lg' ? 'large' : 'medium';
return <MuiButton variant={muiVariant} size={muiSize} {...props} />;
}
// App nutzt eigenen Button
import { Button } from './components/Button';
<Button variant="primary" size="lg">Click me</Button>
Jetzt ist nur Button.tsx abhängig von MUI. Wechsel zu
Chakra? Nur eine Datei ändern.
Jede Library bringt eigene Type-Definitionen.
// Material UI
import { ButtonProps } from '@mui/material';
function CustomButton(props: ButtonProps) {
return <Button {...props} />;
}
// Erweitern mit eigenen Props
interface CustomButtonProps extends ButtonProps {
loading?: boolean;
}
// PrimeReact
import { DataTableProps } from 'primereact/datatable';
interface CustomTableProps extends DataTableProps {
onExport?: () => void;
}
// Chakra UI
import { BoxProps } from '@chakra-ui/react';
interface CardProps extends BoxProps {
title: string;
}
function Card({ title, children, ...boxProps }: CardProps) {
return (
<Box p={4} borderWidth={1} borderRadius="md" {...boxProps}>
<Text fontSize="xl" fontWeight="bold">{title}</Text>
{children}
</Box>
);
}
Fehler 1: CSS-Imports in falscher Reihenfolge
// ❌ Falsch: Eigenes CSS vor Library-CSS
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
// App.css wird von Bootstrap überschrieben!
// ✓ Richtig: Library zuerst, dann Overrides
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
Fehler 2: ThemeProvider vergessen
// ❌ Falsch: MUI-Komponenten ohne Provider
function App() {
return <Button variant="contained">Click</Button>;
// Default-Theme, keine Custom-Colors
}
// ✓ Richtig: ThemeProvider umschließt App
function App() {
return (
<ThemeProvider theme={customTheme}>
<Button variant="contained">Click</Button>
</ThemeProvider>
);
}
Fehler 3: Globale Styles überschreiben versehentlich Library
/* ❌ Falsch: Zu generische Selektoren */
button {
background: red; /* Überschreibt alle Library-Buttons! */
}
/* ✓ Richtig: Spezifische Klassen */
.my-button {
background: red;
}Fehler 4: Icons nicht importiert
// ❌ Falsch: PrimeReact-Icons fehlen
import { Button } from 'primereact/button';
<Button icon="pi pi-check" /> // Icon nicht sichtbar!
// ✓ Richtig: Icons importieren
import 'primeicons/primeicons.css';
Fehler 5: Incompatible Library-Versionen
# ❌ Falsch: MUI v5 mit React 17
npm install @mui/material@5.0.0
# Braucht React 18!
# ✓ Richtig: Kompatible Versionen prüfen
npm install @mui/material@latest react@latest react-dom@latestJede Library hat eigene Dark-Mode-Mechanismen.
Material UI:
import { createTheme, ThemeProvider } from '@mui/material';
import { useState } from 'react';
function App() {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const theme = createTheme({
palette: {
mode,
},
});
return (
<ThemeProvider theme={theme}>
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
Toggle Dark Mode
</button>
</ThemeProvider>
);
}
Chakra UI:
import { ChakraProvider, useColorMode, Button } from '@chakra-ui/react';
function ThemeToggle() {
const { colorMode, toggleColorMode } = useColorMode();
return (
<Button onClick={toggleColorMode}>
Toggle {colorMode === 'light' ? 'Dark' : 'Light'}
</Button>
);
}
PrimeReact:
// Lade verschiedene Theme-CSS je nach Mode
const [theme, setTheme] = useState('lara-light-indigo');
useEffect(() => {
import(`primereact/resources/themes/${theme}/theme.css`);
}, [theme]);
<button onClick={() => setTheme(
theme === 'lara-light-indigo' ? 'lara-dark-indigo' : 'lara-light-indigo'
)}>
Toggle Theme
</button>
Schrittweise Migration statt Big Bang:
// Phase 1: Beide Libraries koexistieren
import { Button as OldButton } from 'old-library';
import { Button as NewButton } from 'new-library';
// Route 1: Noch alte Library
<Route path="/old-dashboard" element={<OldDashboard />} />
// Route 2: Schon neue Library
<Route path="/new-dashboard" element={<NewDashboard />} />
UI-Bibliotheken sind Werkzeuge, keine Religion. Bootstrap für interne Tools? Perfekt. Material UI für Enterprise-App mit Google-Aesthetik? Gut. Headless UI für komplett custom Design? Auch gut. Die beste Library ist die, die zur Team-Expertise, Projektanforderungen und Deadline passt – nicht die mit den meisten GitHub-Stars.