Mantine - Kompletna Biblioteka React z 100+ Komponentami
Czym jest Mantine?
Mantine to feature-rich biblioteka React, która oferuje wszystko czego potrzebujesz do budowy nowoczesnych aplikacji webowych. Z ponad 100 komponentami, 50+ hookami, wbudowanym systemem formularzy i pełnym wsparciem TypeScript - Mantine to jedno z najbardziej kompletnych rozwiązań UI dla React.
W przeciwieństwie do minimalnych bibliotek jak Radix UI czy Headless UI, Mantine oferuje gotowe do użycia, w pełni wystylizowane komponenty. Ale w odróżnieniu od Bootstrap czy MUI, Mantine jest nowoczesny, lekki i zaprojektowany z myślą o developer experience.
Dlaczego Mantine?
Kluczowe zalety Mantine
- 100+ komponentów - Od podstawowych buttonów po zaawansowane tabele, edytory tekstu i charty
- 50+ hooks - Utility hooks do codziennej pracy
- Wbudowane formularze - @mantine/form z walidacją
- Pełny TypeScript - Świetne typowanie i autocomplete
- Theming - Elastyczny system tematów z dark mode
- Zero runtime CSS - PostCSS modules dla optymalnej wydajności
- Dostępność - Komponenty zgodne z WAI-ARIA
Mantine vs inne biblioteki
| Cecha | Mantine | Chakra UI | MUI | Radix + Tailwind |
|---|---|---|---|---|
| Komponenty | 100+ | 60+ | 80+ | 30+ primitive |
| Hooks | 50+ | 20+ | 10+ | 0 |
| Forms | Wbudowane | Zewnętrzne | Zewnętrzne | Zewnętrzne |
| TypeScript | Natywny | Natywny | Natywny | Natywny |
| Bundle size | 40-150KB | 30-100KB | 100-300KB | 10-50KB |
| Styling | CSS Modules | Emotion | Emotion/CSS | Tailwind |
| Customizacja | Wysoka | Wysoka | Średnia | Pełna |
Instalacja i konfiguracja
Podstawowa instalacja
# Core + Hooks
npm install @mantine/core @mantine/hooks
# Z formularzami
npm install @mantine/core @mantine/hooks @mantine/form
# Pełna instalacja (wszystkie pakiety)
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="pl">
<head>
<ColorSchemeScript />
</head>
<body>
<MantineProvider>
<Notifications />
{children}
</MantineProvider>
</body>
</html>
)
}PostCSS setup (wymagane dla 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',
},
},
},
}# Instalacja PostCSS plugins
npm install -D postcss postcss-preset-mantine postcss-simple-varsPodstawowe komponenty
Button
import { Button, Group, Stack } from '@mantine/core'
function ButtonDemo() {
return (
<Stack>
{/* Warianty */}
<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>
{/* Kolory */}
<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>
{/* Rozmiary */}
<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>
{/* Stany */}
<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 i Title
import { Text, Title, Highlight, Mark, Anchor } from '@mantine/core'
function TextDemo() {
return (
<>
{/* Title (nagłówki 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>
{/* Kolory */}
<Text c="blue">Blue text</Text>
<Text c="red.6">Red shade 6</Text>
<Text c="dimmed">Dimmed (gray)</Text>
{/* Formatowanie */}
<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 to biblioteka React z świetnymi komponentami
</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>
)
}
// Karta z sekcjami
function AdvancedCard() {
return (
<Card withBorder radius="md" p="md">
<Card.Section withBorder inheritPadding py="xs">
<Group justify="space-between">
<Text fw={500}>Card header</Text>
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="subtle" color="gray">
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEdit size={14} />}>Edit</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />} color="red">
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Card.Section>
<Text mt="sm" c="dimmed" size="sm">
Card content goes here. This section has padding.
</Text>
<Card.Section inheritPadding mt="sm" pb="md">
<SimpleGrid cols={3}>
{/* Images or other content */}
</SimpleGrid>
</Card.Section>
</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" swatches={['#25262b', '#868e96', '#fa5252', '#e64980', '#be4bdb', '#7950f2', '#4c6ef5', '#228be6', '#15aabf', '#12b886', '#40c057', '#82c91e', '#fab005', '#fd7e14']} />
<FileInput
label="Upload file"
placeholder="Pick file"
accept="image/png,image/jpeg"
/>
</Stack>
)
}Mantine Form
Podstawowa walidacja
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>
)
}Walidacja z 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>
)
}Dynamiczne pola formularza
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>
{/* Podstawowe powiadomienie */}
<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>
{/* Auto close */}
<Button
onClick={() =>
notifications.show({
title: 'Auto close',
message: 'This notification will close in 5 seconds',
autoClose: 5000,
})
}
>
Auto close (5s)
</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'
// Podstawowy 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 (z @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>
}
// Modal z custom content
function ContentModal() {
const openContentModal = () =>
modals.open({
title: 'Subscribe to newsletter',
children: (
<Stack>
<TextInput label="Email" placeholder="you@example.com" />
<Button fullWidth onClick={() => modals.closeAll()}>
Subscribe
</Button>
</Stack>
),
})
return <Button onClick={openContentModal}>Subscribe</Button>
}Mantine Hooks
import {
useDisclosure,
useToggle,
useClipboard,
useMediaQuery,
useLocalStorage,
useDebouncedValue,
useHover,
useClickOutside,
useDocumentTitle,
useFullscreen,
useIdle,
useNetwork,
useOs,
useWindowScroll,
useIntersection,
useMouse,
} from '@mantine/hooks'
// useDisclosure - boolean state z 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 - przełączanie między wartościami
function ToggleDemo() {
const [value, toggle] = useToggle(['light', 'dark'])
return <Button onClick={() => toggle()}>Theme: {value}</Button>
}
// useClipboard - kopiowanie do schowka
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)
// debounced aktualizuje się 300ms po ostatniej zmianie value
useEffect(() => {
if (debounced) {
fetchResults(debounced)
}
}, [debounced])
return (
<TextInput
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
placeholder="Search..."
/>
)
}
// useHover - śledzenie 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>
)
}
// useClickOutside - wykrywanie kliknięć poza elementem
function ClickOutsideDemo() {
const [opened, { close }] = useDisclosure(true)
const ref = useClickOutside(() => close())
return opened ? (
<Paper ref={ref} p="md" shadow="sm">
Click outside to close
</Paper>
) : null
}
// useDocumentTitle - dynamiczny title strony
function TitleDemo() {
useDocumentTitle('My App | Dashboard')
return <div>Check the browser tab title</div>
}
// useNetwork - stan połączenia sieciowego
function NetworkDemo() {
const networkStatus = useNetwork()
return (
<Badge color={networkStatus.online ? 'green' : 'red'}>
{networkStatus.online ? 'Online' : 'Offline'}
</Badge>
)
}
// useOs - wykrywanie systemu operacyjnego
function OsDemo() {
const os = useOs()
return <Text>You're using: {os}</Text>
}
// useWindowScroll - pozycja scrolla
function ScrollDemo() {
const [scroll, scrollTo] = useWindowScroll()
return (
<>
<Text>Scroll position: {scroll.y}px</Text>
<Button onClick={() => scrollTo({ y: 0 })}>Scroll to top</Button>
</>
)
}Theming
Custom theme
import { createTheme, MantineProvider } from '@mantine/core'
const theme = createTheme({
// Kolory
primaryColor: 'violet',
colors: {
brand: [
'#f3e8ff',
'#e9d5ff',
'#d8b4fe',
'#c084fc',
'#a855f7',
'#9333ea',
'#7c3aed',
'#6d28d9',
'#5b21b6',
'#4c1d95',
],
},
// Fonty
fontFamily: 'Inter, sans-serif',
fontFamilyMonospace: 'JetBrains Mono, monospace',
headings: {
fontFamily: 'Inter, sans-serif',
fontWeight: '700',
sizes: {
h1: { fontSize: '2.5rem', lineHeight: '1.2' },
h2: { fontSize: '2rem', lineHeight: '1.3' },
h3: { fontSize: '1.5rem', lineHeight: '1.4' },
},
},
// 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',
},
// Breakpoints
breakpoints: {
xs: '36em',
sm: '48em',
md: '62em',
lg: '75em',
xl: '88em',
},
// Component defaults
components: {
Button: {
defaultProps: {
radius: 'md',
},
styles: {
root: {
fontWeight: 600,
},
},
},
Card: {
defaultProps: {
padding: 'lg',
radius: 'md',
withBorder: true,
},
},
TextInput: {
defaultProps: {
radius: 'md',
},
},
},
// Other
defaultRadius: 'md',
cursorType: 'pointer',
focusRing: 'auto',
})
function App() {
return (
<MantineProvider theme={theme}>
{/* Your app */}
</MantineProvider>
)
}Dark mode
import { MantineProvider, ColorSchemeScript, useMantineColorScheme, Button, Group } from '@mantine/core'
// W 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>
)
}Zaawansowane komponenty
DataTable z sortowaniem i filtrowaniem
import { Table, TextInput, ScrollArea, UnstyledButton, Group, Text, Center } from '@mantine/core'
import { IconSelector, IconChevronDown, IconChevronUp, IconSearch } from '@tabler/icons-react'
import { useState, useMemo } from 'react'
interface RowData {
id: string
name: string
email: string
role: string
}
const data: RowData[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
// ... more data
]
function DataTable() {
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState<keyof RowData | null>(null)
const [reverseSortDirection, setReverseSortDirection] = useState(false)
const setSorting = (field: keyof RowData) => {
const reversed = field === sortBy ? !reverseSortDirection : false
setReverseSortDirection(reversed)
setSortBy(field)
}
const filteredData = useMemo(() => {
let filtered = [...data]
if (search) {
filtered = filtered.filter((item) =>
Object.values(item).some((value) =>
value.toLowerCase().includes(search.toLowerCase())
)
)
}
if (sortBy) {
filtered.sort((a, b) => {
if (reverseSortDirection) {
return b[sortBy].localeCompare(a[sortBy])
}
return a[sortBy].localeCompare(b[sortBy])
})
}
return filtered
}, [search, sortBy, reverseSortDirection])
const rows = filteredData.map((row) => (
<Table.Tr key={row.id}>
<Table.Td>{row.name}</Table.Td>
<Table.Td>{row.email}</Table.Td>
<Table.Td>{row.role}</Table.Td>
</Table.Tr>
))
return (
<>
<TextInput
placeholder="Search..."
leftSection={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
mb="md"
/>
<ScrollArea>
<Table horizontalSpacing="md" verticalSpacing="xs" striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Th
sorted={sortBy === 'name'}
reversed={reverseSortDirection}
onSort={() => setSorting('name')}
>
Name
</Th>
<Th
sorted={sortBy === 'email'}
reversed={reverseSortDirection}
onSort={() => setSorting('email')}
>
Email
</Th>
<Th
sorted={sortBy === 'role'}
reversed={reverseSortDirection}
onSort={() => setSorting('role')}
>
Role
</Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ScrollArea>
</>
)
}
// Sortable header component
function Th({ children, reversed, sorted, onSort }) {
const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector
return (
<Table.Th>
<UnstyledButton onClick={onSort}>
<Group justify="space-between">
<Text fw={500} fz="sm">
{children}
</Text>
<Center>
<Icon size={14} stroke={1.5} />
</Center>
</Group>
</UnstyledButton>
</Table.Th>
)
}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>
)
}Integracja z Next.js App Router
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 '@mantine/carousel/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 - Najczęściej zadawane pytania
Jak Mantine wypada w porównaniu do Chakra UI?
Mantine oferuje więcej komponentów (100+ vs 60+), więcej hooks (50+ vs 20+) i wbudowany system formularzy. Chakra jest nieco lżejszy i ma prostsze API. Mantine jest lepszym wyborem dla większych aplikacji wymagających zaawansowanych komponentów.
Czy Mantine działa z Next.js App Router?
Tak, Mantine 7+ ma pełne wsparcie dla Next.js App Router i Server Components. Komponenty działają zarówno w server jak i client components.
Jak zmniejszyć bundle size?
Mantine używa tree-shaking, więc importuj tylko to czego używasz. Zamiast import { Button, Text } from '@mantine/core' możesz użyć path imports. Ale w praktyce bundlery nowoczesne radzą sobie z tree-shaking bardzo dobrze.
Czy mogę używać Mantine z Tailwind CSS?
Tak, ale nie jest to zalecane. Mantine ma własny system stylowania (CSS Modules) i używanie dwóch systemów naraz może prowadzić do konfliktów i niepotrzebnie zwiększonego bundle size.
Jak customizować komponenty globalnie?
Użyj createTheme() z components property. Możesz ustawić defaultProps, styles i variants dla każdego komponentu globalnie.
Podsumowanie
Mantine to jedna z najbardziej kompletnych bibliotek React, oferująca:
- 100+ komponentów - Od podstawowych po zaawansowane
- 50+ hooks - Utility hooks do codziennej pracy
- Wbudowane formularze - @mantine/form z walidacją i Zod
- Pełny TypeScript - Świetne typowanie i autocomplete
- Theming - Elastyczny system z dark mode
- Next.js support - Działa z App Router i Server Components
Jeśli budujesz średnią lub dużą aplikację React i potrzebujesz kompletnego zestawu narzędzi UI, Mantine jest doskonałym wyborem.