Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide16 min read

Mantine - Kompletny Przewodnik po Bibliotece React z 100+ Komponentami

Mantine is a complete React library with 100+ components, 50+ hooks, and built-in form system. Discover its full capabilities, theming, hooks, and Next.js integration.

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

  1. 100+ components - From basic buttons to advanced tables, text editors, and charts
  2. 50+ hooks - Utility hooks for everyday development tasks
  3. Built-in forms - @mantine/form with validation support
  4. Full TypeScript - Excellent typing and autocomplete
  5. Theming - Flexible theme system with dark mode
  6. Zero runtime CSS - PostCSS modules for optimal performance
  7. Accessibility - WAI-ARIA compliant components

Mantine vs other libraries

FeatureMantineChakra UIMUIRadix + Tailwind
Components100+60+80+30+ primitives
Hooks50+20+10+0
FormsBuilt-inExternalExternalExternal
TypeScriptNativeNativeNativeNative
Bundle size40-150KB30-100KB100-300KB10-50KB
StylingCSS ModulesEmotionEmotion/CSSTailwind
CustomizationHighHighMediumFull

Installation and setup

Basic installation

Code
Bash
# 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 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="en">
      <head>
        <ColorSchemeScript />
      </head>
      <body>
        <MantineProvider>
          <Notifications />
          {children}
        </MantineProvider>
      </body>
    </html>
  )
}

PostCSS setup (required for 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
# Install PostCSS plugins
npm install -D postcss postcss-preset-mantine postcss-simple-vars

Basic components

Button

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

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

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>
  )
}

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" />

      <FileInput
        label="Upload file"
        placeholder="Pick file"
        accept="image/png,image/jpeg"
      />
    </Stack>
  )
}

Mantine Form

Basic validation

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>
  )
}

Validation with 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>
  )
}

Dynamic form fields

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>
      {/* 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

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

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

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

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

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>
  )
}

Next.js App Router integration

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 { 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 - 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.