Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik18 min czytania

Mantine - Kompletny Przewodnik po Bibliotece React z 100+ Komponentami

Mantine to kompletna biblioteka React z ponad 100 komponentami, 50+ hookami i wbudowanym systemem formularzy. Poznaj pełne możliwości biblioteki, tematy, hooks i integrację z Next.js.

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

  1. 100+ komponentów - Od podstawowych buttonów po zaawansowane tabele, edytory tekstu i charty
  2. 50+ hooks - Utility hooks do codziennej pracy
  3. Wbudowane formularze - @mantine/form z walidacją
  4. Pełny TypeScript - Świetne typowanie i autocomplete
  5. Theming - Elastyczny system tematów z dark mode
  6. Zero runtime CSS - PostCSS modules dla optymalnej wydajności
  7. Dostępność - Komponenty zgodne z WAI-ARIA

Mantine vs inne biblioteki

CechaMantineChakra UIMUIRadix + Tailwind
Komponenty100+60+80+30+ primitive
Hooks50+20+10+0
FormsWbudowaneZewnętrzneZewnętrzneZewnętrzne
TypeScriptNatywnyNatywnyNatywnyNatywny
Bundle size40-150KB30-100KB100-300KB10-50KB
StylingCSS ModulesEmotionEmotion/CSSTailwind
CustomizacjaWysokaWysokaŚredniaPełna

Instalacja i konfiguracja

Podstawowa instalacja

Code
Bash
# 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 dayjs

Provider setup

TSapp/layout.tsx
TypeScript
// 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+)

JSpostcss.config.js
JavaScript
// 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',
      },
    },
  },
}
Code
Bash
# Instalacja PostCSS plugins
npm install -D postcss postcss-preset-mantine postcss-simple-vars

Podstawowe komponenty

Button

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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)

Code
TypeScript
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

Code
TypeScript
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

TSapp/layout.tsx
TypeScript
// 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

Code
TypeScript
// 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.