Mantine - complete React library with 100+ components
Building a React application often means piecing together dozens of separate libraries: one for forms, another for notifications, yet another for modals, and so on. Each has its own API, its own styling approach, and its own quirks. Mantine takes a different approach: what if one library could handle everything, with consistent design, excellent TypeScript support, and a developer experience that doesn't make you want to flip your desk?
With over 100 components, 50+ utility hooks, built-in form management, and seamless dark mode support, Mantine has become one of the most complete React UI solutions available. This guide covers everything from basic setup to advanced patterns that will help you build polished applications faster.
What is Mantine?
Mantine is a feature-rich React library that provides everything you need to build modern web applications. With over 100 components, 50+ hooks, a built-in form system, and full TypeScript support, Mantine stands as one of the most complete UI solutions for React.
Unlike minimal libraries like Radix UI or Headless UI, Mantine offers ready-to-use, fully styled components out of the box. But unlike Bootstrap or MUI, Mantine is modern, lightweight, and designed with developer experience as a priority.
Why choose Mantine?
Key advantages
- 100+ components - From basic buttons to advanced tables, text editors, and charts
- 50+ hooks - Utility hooks for everyday development tasks
- Built-in forms - @mantine/form with validation support
- Full TypeScript - Excellent typing and autocomplete
- Theming - Flexible theme system with dark mode
- Zero runtime CSS - PostCSS modules for optimal performance
- Accessibility - WAI-ARIA compliant components
Mantine vs other libraries
| Feature | Mantine | Chakra UI | MUI | Radix + Tailwind |
|---|---|---|---|---|
| Components | 100+ | 60+ | 80+ | 30+ primitives |
| Hooks | 50+ | 20+ | 10+ | 0 |
| Forms | Built-in | External | External | External |
| TypeScript | Native | Native | Native | Native |
| Bundle size | 40-150KB | 30-100KB | 100-300KB | 10-50KB |
| Styling | CSS Modules | Emotion | Emotion/CSS | Tailwind |
| Customization | High | High | Medium | Full |
Installation and setup
Basic installation
# Core + Hooks
npm install @mantine/core @mantine/hooks
# With forms
npm install @mantine/core @mantine/hooks @mantine/form
# Full installation (all packages)
npm install @mantine/core @mantine/hooks @mantine/form @mantine/dates @mantine/notifications @mantine/modals @mantine/spotlight @mantine/dropzone @mantine/carousel @mantine/nprogress dayjsProvider setup
// app/layout.tsx (Next.js App Router)
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import '@mantine/dates/styles.css'
import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<ColorSchemeScript />
</head>
<body>
<MantineProvider>
<Notifications />
{children}
</MantineProvider>
</body>
</html>
)
}PostCSS setup (required for v7+)
// postcss.config.js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
}# Install PostCSS plugins
npm install -D postcss postcss-preset-mantine postcss-simple-varsBasic components
Button
import { Button, Group, Stack } from '@mantine/core'
function ButtonDemo() {
return (
<Stack>
{/* Variants */}
<Group>
<Button>Filled (default)</Button>
<Button variant="light">Light</Button>
<Button variant="outline">Outline</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="transparent">Transparent</Button>
<Button variant="white">White</Button>
<Button variant="default">Default</Button>
</Group>
{/* Colors */}
<Group>
<Button color="blue">Blue</Button>
<Button color="green">Green</Button>
<Button color="red">Red</Button>
<Button color="violet">Violet</Button>
<Button color="orange">Orange</Button>
</Group>
{/* Sizes */}
<Group>
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra large</Button>
</Group>
{/* States */}
<Group>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
<Button leftSection={<IconPlus size={14} />}>With icon</Button>
</Group>
{/* Gradient */}
<Button
variant="gradient"
gradient={{ from: 'indigo', to: 'cyan', deg: 45 }}
>
Gradient button
</Button>
</Stack>
)
}Text and Title
import { Text, Title, Highlight, Mark, Anchor } from '@mantine/core'
function TextDemo() {
return (
<>
{/* Title (headings h1-h6) */}
<Title order={1}>Heading 1</Title>
<Title order={2}>Heading 2</Title>
<Title order={3} c="dimmed">Dimmed heading</Title>
{/* Text */}
<Text size="xl" fw={700}>Extra large bold text</Text>
<Text size="lg">Large text</Text>
<Text>Default text</Text>
<Text size="sm" c="dimmed">Small dimmed text</Text>
{/* Colors */}
<Text c="blue">Blue text</Text>
<Text c="red.6">Red shade 6</Text>
<Text c="dimmed">Dimmed (gray)</Text>
{/* Formatting */}
<Text fs="italic">Italic</Text>
<Text td="underline">Underlined</Text>
<Text td="line-through">Strikethrough</Text>
<Text tt="uppercase">uppercase</Text>
<Text tt="capitalize">capitalize</Text>
{/* Highlight */}
<Highlight highlight={['React', 'Mantine']}>
Mantine is a React library with great components
</Highlight>
{/* Mark */}
<Text>
This is <Mark>highlighted</Mark> text
</Text>
{/* Link */}
<Anchor href="https://mantine.dev" target="_blank">
Mantine documentation
</Anchor>
</>
)
}Card
import { Card, Image, Text, Badge, Button, Group } from '@mantine/core'
function CardDemo() {
return (
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Card.Section>
<Image
src="https://images.unsplash.com/photo-1527004013197-933c4bb611b3"
height={160}
alt="Norway"
/>
</Card.Section>
<Group justify="space-between" mt="md" mb="xs">
<Text fw={500}>Norway Fjord Adventures</Text>
<Badge color="pink">On Sale</Badge>
</Group>
<Text size="sm" c="dimmed">
With Fjord Tours you can explore more of the magical fjord landscapes
with tours and activities on and around the fjords of Norway
</Text>
<Button color="blue" fullWidth mt="md" radius="md">
Book classic tour now
</Button>
</Card>
)
}Input components
import {
TextInput,
PasswordInput,
Textarea,
NumberInput,
Select,
MultiSelect,
Checkbox,
Radio,
Switch,
Slider,
RangeSlider,
ColorInput,
FileInput,
} from '@mantine/core'
function InputsDemo() {
return (
<Stack>
{/* Text inputs */}
<TextInput
label="Email"
placeholder="you@example.com"
description="Enter your email address"
error="Invalid email"
withAsterisk
/>
<PasswordInput
label="Password"
placeholder="Your password"
description="Must be at least 8 characters"
/>
<Textarea
label="Description"
placeholder="Enter description"
autosize
minRows={3}
maxRows={6}
/>
<NumberInput
label="Quantity"
placeholder="Enter quantity"
min={0}
max={100}
step={1}
defaultValue={1}
/>
{/* Select */}
<Select
label="Framework"
placeholder="Pick one"
data={['React', 'Angular', 'Vue', 'Svelte']}
searchable
clearable
/>
<MultiSelect
label="Technologies"
placeholder="Pick all that apply"
data={['TypeScript', 'JavaScript', 'Python', 'Go', 'Rust']}
searchable
maxSelectedValues={3}
/>
{/* Toggles */}
<Checkbox label="I agree to terms and conditions" />
<Radio.Group name="plan" label="Select plan">
<Group mt="xs">
<Radio value="free" label="Free" />
<Radio value="pro" label="Pro" />
<Radio value="enterprise" label="Enterprise" />
</Group>
</Radio.Group>
<Switch label="Enable notifications" />
{/* Sliders */}
<Slider
label="Volume"
marks={[
{ value: 0, label: '0%' },
{ value: 50, label: '50%' },
{ value: 100, label: '100%' },
]}
/>
<RangeSlider
label="Price range"
min={0}
max={1000}
step={10}
minRange={50}
/>
{/* Special inputs */}
<ColorInput label="Pick color" format="hex" />
<FileInput
label="Upload file"
placeholder="Pick file"
accept="image/png,image/jpeg"
/>
</Stack>
)
}Mantine Form
Basic validation
import { useForm } from '@mantine/form'
import { TextInput, Button, Box, PasswordInput, Checkbox } from '@mantine/core'
interface FormValues {
email: string
password: string
confirmPassword: string
termsAccepted: boolean
}
function RegistrationForm() {
const form = useForm<FormValues>({
initialValues: {
email: '',
password: '',
confirmPassword: '',
termsAccepted: false,
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
password: (value) =>
value.length < 8 ? 'Password must be at least 8 characters' : null,
confirmPassword: (value, values) =>
value !== values.password ? 'Passwords do not match' : null,
termsAccepted: (value) =>
value ? null : 'You must accept terms and conditions',
},
})
const handleSubmit = (values: FormValues) => {
console.log('Form submitted:', values)
}
return (
<Box maw={400} mx="auto">
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Email"
placeholder="you@example.com"
withAsterisk
{...form.getInputProps('email')}
/>
<PasswordInput
label="Password"
placeholder="Your password"
withAsterisk
mt="md"
{...form.getInputProps('password')}
/>
<PasswordInput
label="Confirm password"
placeholder="Confirm your password"
withAsterisk
mt="md"
{...form.getInputProps('confirmPassword')}
/>
<Checkbox
label="I accept terms and conditions"
mt="md"
{...form.getInputProps('termsAccepted', { type: 'checkbox' })}
/>
<Button type="submit" fullWidth mt="xl">
Register
</Button>
</form>
</Box>
)
}Validation with Zod
import { useForm, zodResolver } from '@mantine/form'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[0-9]/, 'Password must contain at least one number'),
age: z.number().min(18, 'You must be at least 18 years old'),
})
type FormValues = z.infer<typeof schema>
function ZodForm() {
const form = useForm<FormValues>({
initialValues: {
email: '',
password: '',
age: 18,
},
validate: zodResolver(schema),
})
return (
<form onSubmit={form.onSubmit(console.log)}>
<TextInput label="Email" {...form.getInputProps('email')} />
<PasswordInput label="Password" {...form.getInputProps('password')} />
<NumberInput label="Age" {...form.getInputProps('age')} />
<Button type="submit" mt="md">
Submit
</Button>
</form>
)
}Dynamic form fields
import { useForm, isNotEmpty } from '@mantine/form'
import { TextInput, Button, Group, ActionIcon, Box } from '@mantine/core'
import { IconTrash, IconPlus } from '@tabler/icons-react'
function DynamicForm() {
const form = useForm({
initialValues: {
employees: [{ name: '', email: '' }],
},
validate: {
employees: {
name: isNotEmpty('Name is required'),
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
},
},
})
const fields = form.values.employees.map((_, index) => (
<Group key={index} mt="xs">
<TextInput
placeholder="Name"
style={{ flex: 1 }}
{...form.getInputProps(`employees.${index}.name`)}
/>
<TextInput
placeholder="Email"
style={{ flex: 1 }}
{...form.getInputProps(`employees.${index}.email`)}
/>
<ActionIcon
color="red"
onClick={() => form.removeListItem('employees', index)}
disabled={form.values.employees.length === 1}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
))
return (
<Box maw={500} mx="auto">
{fields}
<Group justify="center" mt="md">
<Button
leftSection={<IconPlus size={14} />}
onClick={() =>
form.insertListItem('employees', { name: '', email: '' })
}
>
Add employee
</Button>
</Group>
<Button type="submit" fullWidth mt="xl">
Submit
</Button>
</Box>
)
}Notifications
import { Button, Group } from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { IconCheck, IconX } from '@tabler/icons-react'
function NotificationsDemo() {
return (
<Group>
{/* Basic notification */}
<Button
onClick={() =>
notifications.show({
title: 'Default notification',
message: 'This is a default notification',
})
}
>
Show notification
</Button>
{/* Success */}
<Button
color="green"
onClick={() =>
notifications.show({
title: 'Success!',
message: 'Your changes have been saved',
color: 'green',
icon: <IconCheck size={18} />,
})
}
>
Success
</Button>
{/* Error */}
<Button
color="red"
onClick={() =>
notifications.show({
title: 'Error!',
message: 'Something went wrong',
color: 'red',
icon: <IconX size={18} />,
})
}
>
Error
</Button>
{/* Loading → Success */}
<Button
onClick={() => {
const id = notifications.show({
loading: true,
title: 'Loading data',
message: 'Please wait...',
autoClose: false,
withCloseButton: false,
})
setTimeout(() => {
notifications.update({
id,
color: 'green',
title: 'Data loaded',
message: 'Everything is ready',
icon: <IconCheck size={18} />,
loading: false,
autoClose: 2000,
})
}, 3000)
}}
>
Loading → Success
</Button>
</Group>
)
}Modals
import { useDisclosure } from '@mantine/hooks'
import { Modal, Button, Group, TextInput, Stack } from '@mantine/core'
import { modals } from '@mantine/modals'
// Basic modal
function BasicModal() {
const [opened, { open, close }] = useDisclosure(false)
return (
<>
<Modal opened={opened} onClose={close} title="Authentication" centered>
<Stack>
<TextInput label="Email" placeholder="you@example.com" />
<TextInput label="Password" type="password" placeholder="Password" />
<Button onClick={close}>Login</Button>
</Stack>
</Modal>
<Button onClick={open}>Open modal</Button>
</>
)
}
// Confirmation modal (with @mantine/modals)
function ConfirmModal() {
const openDeleteModal = () =>
modals.openConfirmModal({
title: 'Delete account',
centered: true,
children: (
<Text size="sm">
Are you sure you want to delete your account? This action is
irreversible.
</Text>
),
labels: { confirm: 'Delete account', cancel: "No, don't delete" },
confirmProps: { color: 'red' },
onCancel: () => console.log('Cancel'),
onConfirm: () => console.log('Confirmed'),
})
return <Button color="red" onClick={openDeleteModal}>Delete account</Button>
}Mantine Hooks
import {
useDisclosure,
useToggle,
useClipboard,
useMediaQuery,
useLocalStorage,
useDebouncedValue,
useHover,
useClickOutside,
useDocumentTitle,
useNetwork,
useOs,
useWindowScroll,
} from '@mantine/hooks'
// useDisclosure - boolean state with open/close/toggle
function DisclosureDemo() {
const [opened, { open, close, toggle }] = useDisclosure(false)
return (
<>
<Button onClick={open}>Open</Button>
<Button onClick={close}>Close</Button>
<Button onClick={toggle}>Toggle</Button>
</>
)
}
// useToggle - toggle between values
function ToggleDemo() {
const [value, toggle] = useToggle(['light', 'dark'])
return <Button onClick={() => toggle()}>Theme: {value}</Button>
}
// useClipboard - copy to clipboard
function ClipboardDemo() {
const clipboard = useClipboard({ timeout: 500 })
return (
<Button
color={clipboard.copied ? 'green' : 'blue'}
onClick={() => clipboard.copy('Hello, World!')}
>
{clipboard.copied ? 'Copied!' : 'Copy'}
</Button>
)
}
// useMediaQuery - responsive hooks
function MediaQueryDemo() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
return (
<Text>
Device: {isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop'}
</Text>
)
}
// useLocalStorage - persisted state
function LocalStorageDemo() {
const [value, setValue] = useLocalStorage({
key: 'user-preferences',
defaultValue: { theme: 'light', notifications: true },
})
return (
<Button onClick={() => setValue({ ...value, theme: value.theme === 'light' ? 'dark' : 'light' })}>
Toggle theme: {value.theme}
</Button>
)
}
// useDebouncedValue - debounced input
function DebouncedDemo() {
const [value, setValue] = useState('')
const [debounced] = useDebouncedValue(value, 300)
useEffect(() => {
if (debounced) {
fetchResults(debounced)
}
}, [debounced])
return (
<TextInput
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
placeholder="Search..."
/>
)
}
// useHover - track hover state
function HoverDemo() {
const { hovered, ref } = useHover()
return (
<Box ref={ref} bg={hovered ? 'blue.1' : 'gray.1'} p="md">
{hovered ? 'Hovered!' : 'Hover me'}
</Box>
)
}
// useNetwork - network connection status
function NetworkDemo() {
const networkStatus = useNetwork()
return (
<Badge color={networkStatus.online ? 'green' : 'red'}>
{networkStatus.online ? 'Online' : 'Offline'}
</Badge>
)
}
// useOs - detect operating system
function OsDemo() {
const os = useOs()
return <Text>You're using: {os}</Text>
}Theming
Custom theme
import { createTheme, MantineProvider } from '@mantine/core'
const theme = createTheme({
// Colors
primaryColor: 'violet',
colors: {
brand: [
'#f3e8ff',
'#e9d5ff',
'#d8b4fe',
'#c084fc',
'#a855f7',
'#9333ea',
'#7c3aed',
'#6d28d9',
'#5b21b6',
'#4c1d95',
],
},
// Fonts
fontFamily: 'Inter, sans-serif',
fontFamilyMonospace: 'JetBrains Mono, monospace',
headings: {
fontFamily: 'Inter, sans-serif',
fontWeight: '700',
},
// Spacing
spacing: {
xs: '0.5rem',
sm: '0.75rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
// Border radius
radius: {
xs: '2px',
sm: '4px',
md: '8px',
lg: '16px',
xl: '32px',
},
// Component defaults
components: {
Button: {
defaultProps: {
radius: 'md',
},
styles: {
root: {
fontWeight: 600,
},
},
},
Card: {
defaultProps: {
padding: 'lg',
radius: 'md',
withBorder: true,
},
},
},
defaultRadius: 'md',
})
function App() {
return (
<MantineProvider theme={theme}>
{/* Your app */}
</MantineProvider>
)
}Dark mode
import { MantineProvider, ColorSchemeScript, useMantineColorScheme, Button, Group } from '@mantine/core'
// In layout
function Layout({ children }) {
return (
<html>
<head>
<ColorSchemeScript defaultColorScheme="auto" />
</head>
<body>
<MantineProvider defaultColorScheme="auto">
{children}
</MantineProvider>
</body>
</html>
)
}
// Theme switcher
function ThemeSwitcher() {
const { setColorScheme, colorScheme } = useMantineColorScheme()
return (
<Group>
<Button
variant={colorScheme === 'light' ? 'filled' : 'default'}
onClick={() => setColorScheme('light')}
>
Light
</Button>
<Button
variant={colorScheme === 'dark' ? 'filled' : 'default'}
onClick={() => setColorScheme('dark')}
>
Dark
</Button>
<Button
variant={colorScheme === 'auto' ? 'filled' : 'default'}
onClick={() => setColorScheme('auto')}
>
Auto
</Button>
</Group>
)
}Advanced components
Spotlight (command palette)
import { Spotlight, spotlight } from '@mantine/spotlight'
import { Button } from '@mantine/core'
import { IconSearch, IconHome, IconSettings, IconUser } from '@tabler/icons-react'
const actions = [
{
id: 'home',
label: 'Home',
description: 'Go to home page',
onClick: () => console.log('Home'),
leftSection: <IconHome size={18} />,
},
{
id: 'settings',
label: 'Settings',
description: 'Manage your settings',
onClick: () => console.log('Settings'),
leftSection: <IconSettings size={18} />,
},
{
id: 'profile',
label: 'Profile',
description: 'View your profile',
onClick: () => console.log('Profile'),
leftSection: <IconUser size={18} />,
},
]
function SpotlightDemo() {
return (
<>
<Button onClick={spotlight.open}>Open spotlight (⌘K)</Button>
<Spotlight
actions={actions}
nothingFound="Nothing found..."
highlightQuery
searchProps={{
leftSection: <IconSearch size={18} />,
placeholder: 'Search...',
}}
/>
</>
)
}Dropzone
import { Dropzone, IMAGE_MIME_TYPE, FileWithPath } from '@mantine/dropzone'
import { Group, Text, rem } from '@mantine/core'
import { IconUpload, IconPhoto, IconX } from '@tabler/icons-react'
function DropzoneDemo() {
const handleDrop = (files: FileWithPath[]) => {
console.log('Accepted files:', files)
}
return (
<Dropzone
onDrop={handleDrop}
onReject={(files) => console.log('Rejected files:', files)}
maxSize={5 * 1024 ** 2} // 5MB
accept={IMAGE_MIME_TYPE}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)' }}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)' }}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)' }}
stroke={1.5}
/>
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
)
}Next.js App Router integration
Layout setup
// app/layout.tsx
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import '@mantine/spotlight/styles.css'
import '@mantine/dropzone/styles.css'
import '@mantine/dates/styles.css'
import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import { ModalsProvider } from '@mantine/modals'
import { theme } from './theme'
export const metadata = {
title: 'My App',
description: 'Built with Mantine and Next.js',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<ColorSchemeScript />
</head>
<body>
<MantineProvider theme={theme}>
<ModalsProvider>
<Notifications position="top-right" />
{children}
</ModalsProvider>
</MantineProvider>
</body>
</html>
)
}Server Components
// Mantine components work in Server Components!
// app/page.tsx
import { Title, Text, Container, Card, SimpleGrid } from '@mantine/core'
export default function HomePage() {
return (
<Container size="lg" py="xl">
<Title order={1} mb="lg">Welcome to My App</Title>
<Text size="lg" c="dimmed" mb="xl">
Built with Mantine and Next.js
</Text>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
<Card>
<Title order={3}>Feature 1</Title>
<Text>Description</Text>
</Card>
<Card>
<Title order={3}>Feature 2</Title>
<Text>Description</Text>
</Card>
<Card>
<Title order={3}>Feature 3</Title>
<Text>Description</Text>
</Card>
</SimpleGrid>
</Container>
)
}FAQ - frequently asked questions
How does Mantine compare to Chakra UI?
Mantine offers more components (100+ vs 60+), more hooks (50+ vs 20+), and a built-in form system. Chakra is slightly lighter and has a simpler API. Mantine is a better choice for larger applications requiring advanced components.
Does Mantine work with Next.js App Router?
Yes, Mantine 7+ has full support for Next.js App Router and Server Components. Components work in both server and client components.
How do I reduce bundle size?
Mantine uses tree-shaking, so import only what you use. In practice, modern bundlers handle tree-shaking very well, so you don't need to worry about path imports.
Can I use Mantine with Tailwind CSS?
Yes, but it's not recommended. Mantine has its own styling system (CSS Modules) and using two systems together can lead to conflicts and unnecessarily increased bundle size.
How do I customize components globally?
Use createTheme() with the components property. You can set defaultProps, styles, and variants for each component globally.
Summary
Mantine is one of the most complete React libraries available, offering:
- 100+ components - From basic to advanced
- 50+ hooks - Utility hooks for everyday development
- Built-in forms - @mantine/form with validation and Zod support
- Full TypeScript - Excellent typing and autocomplete
- Theming - Flexible system with dark mode
- Next.js support - Works with App Router and Server Components
If you're building a medium to large React application and need a complete UI toolkit, Mantine is an excellent choice that will save you from juggling dozens of separate libraries.