web-ui-mantine
Mantine v7 Component Patterns
Quick Guide: Mantine v7 is a full-featured React component library using CSS Modules (not CSS-in-JS). Wrap your app in
MantineProviderwith acreateThemeobject. Style with CSS Modules + PostCSS preset mixins, the Styles API (classNames/stylesprops), or style props (p,m,bg, etc.). Use@mantine/formfor form management,@mantine/hooksfor utility hooks, and@mantine/notificationsfor toasts. PostCSS preset withpostcss-preset-mantineis required for responsive mixins andlight-dark().
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST wrap your app in MantineProvider and import @mantine/core/styles.css at the root)
(You MUST install and configure postcss-preset-mantine for CSS Modules mixins and responsive utilities)
(You MUST use CSS Modules (.module.css) for custom component styles - createStyles was removed in v7)
(You MUST provide 10-shade color tuples when adding custom colors to the theme)
</critical_requirements>
Auto-detection: Mantine, @mantine/core, @mantine/hooks, @mantine/form, @mantine/notifications, @mantine/dates, MantineProvider, createTheme, useForm, useDisclosure, useDebouncedValue, useMediaQuery, postcss-preset-mantine, light-dark(), getInputProps, classNames prop, styles prop
When to use:
- Building React applications needing 100+ ready-to-use accessible components
- Projects requiring built-in form management, date pickers, or notification systems
- Applications where CSS Modules (no runtime CSS-in-JS) are preferred for performance
- Teams wanting a batteries-included library with hooks, theming, and responsive utilities
When NOT to use:
- Projects committed to a different component library
- Applications requiring utility-class-first styling (Mantine uses CSS Modules)
- When only a few components are needed and a full library is overkill
Key patterns covered:
- MantineProvider setup with createTheme and PostCSS configuration
- Core components (Button, TextInput, Select, Modal, Drawer, Menu, Tabs, Accordion)
- Styling with CSS Modules, Styles API (classNames/styles/vars), and style props
- Theming with createTheme (colors, defaultRadius, component defaults)
- Color scheme (light/dark/auto) with useMantineColorScheme
- @mantine/form (useForm, validation, nested fields, list management)
- @mantine/hooks (useDisclosure, useDebouncedValue, useMediaQuery)
- @mantine/notifications and @mantine/dates
Philosophy
Mantine is a batteries-included React component library. Unlike copy-and-own systems, Mantine provides pre-built components that are styled via CSS Modules and customized through a theme object and Styles API. The v7 rewrite (late 2023) dropped Emotion for native CSS Modules, eliminating CSS-in-JS runtime overhead entirely.
What Mantine handles vs what other skills handle:
- Mantine: component rendering, theming, form management, hooks, notifications, dates
- Your styling approach: custom CSS beyond Mantine's Styles API
- Your data fetching solution: server state management
- Your state management solution: global client state beyond component state
Core Patterns
Pattern 1: MantineProvider Setup
Every Mantine app requires MantineProvider at the root with CSS imports and PostCSS configuration.
// app.tsx
import "@mantine/core/styles.css";
// Import additional package styles only if using those packages:
// import '@mantine/dates/styles.css';
// import '@mantine/notifications/styles.css';
import { createTheme, MantineProvider } from "@mantine/core";
const theme = createTheme({
primaryColor: "blue",
defaultRadius: "md",
});
export function App() {
return (
<MantineProvider theme={theme}>{/* Your app content */}</MantineProvider>
);
}
PostCSS Configuration
// postcss.config.cjs
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",
},
},
},
};
SSR Setup (Required for SSR Frameworks)
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
// In your root HTML/layout:
<html lang="en" {...mantineHtmlProps}>
<head>
<ColorSchemeScript defaultColorScheme="auto" />
</head>
<body>
<App />
</body>
</html>;
Why good: Theme object lives outside component body (prevents re-renders), PostCSS preset enables responsive mixins, ColorSchemeScript prevents flash of wrong theme on SSR
Pattern 2: Core Components
Mantine components accept className, style, style props, and the Styles API. All support variant, size, color, and radius props.
import {
Button,
TextInput,
Select,
Checkbox,
Group,
Stack,
} from "@mantine/core";
// Button with variants and sizes
<Button variant="filled" color="blue" size="md" loading={isLoading}>
Submit
</Button>
<Button variant="outline">Cancel</Button>
<Button variant="light" color="red" leftSection={<TrashIcon />}>
Delete
</Button>
// Text inputs
<TextInput
label="Email"
placeholder="you@example.com"
withAsterisk
error={errors.email}
/>
// Select
<Select
label="Role"
data={["Admin", "Editor", "Viewer"]}
placeholder="Pick a role"
searchable
clearable
/>
// Checkbox
<Checkbox label="Accept terms" checked={accepted} onChange={handleChange} />
// Layout components
<Group gap="md">{/* Horizontal flex */}</Group>
<Stack gap="sm">{/* Vertical flex */}</Stack>
Why good: Consistent API across all components (variant, size, color, radius), layout components (Group, Stack) replace manual flexbox
Pattern 3: Modal, Drawer, Menu, Tabs, Accordion
Overlay and compound components use useDisclosure for state management.
import { Modal, Button } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
function Demo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Modal opened={opened} onClose={close} title="Confirm Action">
<p>Are you sure?</p>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={close}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</Group>
</Modal>
<Button onClick={open}>Open Modal</Button>
</>
);
}
// Drawer - same pattern as Modal
import { Drawer } from "@mantine/core";
<Drawer opened={opened} onClose={close} title="Settings" position="right">
{/* Drawer content */}
</Drawer>;
// Menu
import { Menu, Button } from "@mantine/core";
<Menu shadow="md" width={200}>
<Menu.Target>
<Button>Actions</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Application</Menu.Label>
<Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
<Menu.Divider />
<Menu.Item color="red" leftSection={<TrashIcon />}>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>;
// Tabs
import { Tabs } from "@mantine/core";
<Tabs defaultValue="general">
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="security">Security</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">General settings...</Tabs.Panel>
<Tabs.Panel value="security">Security settings...</Tabs.Panel>
</Tabs>;
// Accordion
import { Accordion } from "@mantine/core";
<Accordion>
<Accordion.Item value="faq-1">
<Accordion.Control>How does billing work?</Accordion.Control>
<Accordion.Panel>Monthly billing starts on signup date.</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="faq-2">
<Accordion.Control>Can I cancel anytime?</Accordion.Control>
<Accordion.Panel>Yes, cancel anytime from settings.</Accordion.Panel>
</Accordion.Item>
</Accordion>;
Why good: Consistent compound component pattern (Component.Sub), useDisclosure manages open/close state without manual useState boilerplate
Pattern 4: Styling with CSS Modules and Styles API
Mantine v7 uses CSS Modules as the primary styling approach. The PostCSS preset provides responsive mixins and light-dark().
CSS Modules
/* feature-card.module.css */
.card {
padding: var(--mantine-spacing-md);
border-radius: var(--mantine-radius-md);
background-color: light-dark(
var(--mantine-color-white),
var(--mantine-color-dark-6)
);
@mixin hover {
box-shadow: var(--mantine-shadow-md);
}
@mixin smaller-than 48em {
padding: var(--mantine-spacing-sm);
}
}
.title {
font-weight: 700;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
import { Card, Text } from "@mantine/core";
import classes from "./feature-card.module.css";
export function FeatureCard({ title, children }: FeatureCardProps) {
return (
<Card className={classes.card}>
<Text className={classes.title}>{title}</Text>
{children}
</Card>
);
}
Styles API (classNames and styles props)
Every Mantine component exposes named selectors for inner elements:
// Target specific inner elements with classNames
<TextInput
classNames={{
root: classes.inputRoot,
input: classes.inputField,
label: classes.inputLabel,
}}
/>
// Or inline styles for quick overrides
<TextInput
styles={{
input: { backgroundColor: "var(--mantine-color-blue-0)" },
label: { fontWeight: 700 },
}}
/>
Style Props
All Mantine components support shorthand style props with responsive object syntax:
import { Box } from "@mantine/core";
<Box
p="md"
bg="blue.1"
c="blue.9"
w={{ base: "100%", sm: 400, lg: 600 }}
fz="sm"
ta="center"
/>;
Why good: CSS Modules have zero runtime overhead, Styles API targets inner elements without source modification, style props enable rapid prototyping with responsive breakpoints
Pattern 5: Theming with createTheme
Customize the entire library through the theme object. Theme is deeply merged with Mantine defaults.
import {
createTheme,
MantineProvider,
virtualColor,
Button,
} from "@mantine/core";
const BRAND_COLORS = [
"#f0e4ff",
"#d9b8ff",
"#c28dff",
"#aa61ff",
"#9336ff",
"#7b0bff",
"#6700d9",
"#5300b3",
"#3f008c",
"#2b0066",
] as const;
const theme = createTheme({
// Colors - each color is a 10-shade tuple (index 0 lightest, 9 darkest)
colors: {
brand: [...BRAND_COLORS],
// virtualColor switches based on color scheme
surface: virtualColor({
name: "surface",
dark: "dark",
light: "gray",
}),
},
primaryColor: "brand",
primaryShade: { light: 6, dark: 8 },
// Typography
fontFamily: "Inter, sans-serif",
headings: { fontFamily: "Inter, sans-serif" },
// Layout
defaultRadius: "md",
// Component defaults - override props and styles globally
components: {
Button: Button.extend({
defaultProps: {
variant: "filled",
size: "sm",
},
}),
},
});
Why good: Colors are 10-shade tuples enabling automatic shade selection, virtualColor handles dark/light mode switching, component defaults reduce prop repetition across the app
See examples/theming.md for custom color generation, theme extension, and component default override patterns.
Pattern 6: Color Scheme (Light/Dark/Auto)
Mantine supports light, dark, and auto color schemes with localStorage persistence.
import {
useMantineColorScheme,
useComputedColorScheme,
ActionIcon,
} from "@mantine/core";
export function ColorSchemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const computedScheme = useComputedColorScheme("light");
return (
<ActionIcon
variant="outline"
onClick={() =>
setColorScheme(computedScheme === "dark" ? "light" : "dark")
}
>
{computedScheme === "dark" ? <SunIcon /> : <MoonIcon />}
</ActionIcon>
);
}
Critical: Use useComputedColorScheme (not useMantineColorScheme().colorScheme) for toggle logic. When set to "auto", colorScheme returns "auto" not the resolved value.
// MantineProvider configuration for color scheme
<MantineProvider theme={theme} defaultColorScheme="auto">
{/* App */}
</MantineProvider>
Conditional visibility:
// Built-in props for scheme-conditional rendering
<Button lightHidden>Only visible in dark mode</Button>
<Button darkHidden>Only visible in light mode</Button>
Why good: auto respects system preference, localStorage persists user choice, useComputedColorScheme resolves auto to actual value
Pattern 7: @mantine/form (useForm)
Full form management with validation, nested fields, and list operations.
import { useForm } from "@mantine/form";
import { TextInput, Checkbox, Button, Group, Box } from "@mantine/core";
export function SignupForm() {
const form = useForm({
mode: "uncontrolled",
initialValues: {
email: "",
name: "",
termsAccepted: false,
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"),
name: (value) =>
value.length < 2 ? "Name must be at least 2 characters" : null,
termsAccepted: (value) => (value ? null : "You must accept terms"),
},
});
return (
<Box component="form" onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Name"
withAsterisk
key={form.key("name")}
{...form.getInputProps("name")}
/>
<TextInput
label="Email"
withAsterisk
key={form.key("email")}
{...form.getInputProps("email")}
/>
<Checkbox
label="I accept the terms"
key={form.key("termsAccepted")}
{...form.getInputProps("termsAccepted", { type: "checkbox" })}
/>
<Group mt="md" justify="flex-end">
<Button type="submit">Submit</Button>
</Group>
</Box>
);
}
Cross-field Validation
const form = useForm({
mode: "uncontrolled",
initialValues: { password: "", confirmPassword: "" },
validate: {
confirmPassword: (value, values) =>
value !== values.password ? "Passwords do not match" : null,
},
});
Nested Fields and Lists
const form = useForm({
mode: "uncontrolled",
initialValues: {
employees: [{ name: "", email: "" }],
},
});
// Add item
form.insertListItem("employees", { name: "", email: "" });
// Remove item
form.removeListItem("employees", index);
// Access nested fields with dot notation
form.getInputProps("employees.0.name");
form.getInputProps("employees.0.email");
Why good: getInputProps auto-binds value, onChange, and error to inputs; mode: "uncontrolled" (default) avoids re-renders on every keystroke; form.key() is required for uncontrolled mode to maintain React key stability
See examples/forms.md for Zod resolver integration, dynamic list forms, and validation timing options.
Pattern 8: @mantine/hooks
Utility hooks that solve common React patterns.
import { useState } from "react";
import {
useDisclosure,
useDebouncedValue,
useMediaQuery,
useClickOutside,
useClipboard,
} from "@mantine/hooks";
// useDisclosure - boolean state with handlers
const [opened, { open, close, toggle }] = useDisclosure(false);
// useDebouncedValue - debounce any value
const [search, setSearch] = useState("");
const [debounced] = useDebouncedValue(search, 300);
// useMediaQuery - responsive logic in JS
const isMobile = useMediaQuery("(max-width: 48em)");
// useClickOutside - close on outside click
const ref = useClickOutside(() => close());
// useClipboard - copy to clipboard with timeout
const clipboard = useClipboard({ timeout: 2000 });
clipboard.copy("text to copy");
// clipboard.copied is true for 2000ms after copy
Why good: Purpose-built hooks eliminate boilerplate, useDebouncedValue handles cleanup automatically, useMediaQuery returns undefined during SSR (safe for hydration)
Pattern 9: @mantine/notifications
Toast notification system with queue management.
// Root setup (once)
import { Notifications } from "@mantine/notifications";
import "@mantine/notifications/styles.css";
<MantineProvider>
<Notifications position="top-right" />
<App />
</MantineProvider>;
// Show notifications anywhere
import { notifications } from "@mantine/notifications";
// Basic notification
notifications.show({
title: "Success",
message: "Your changes have been saved",
color: "green",
});
// Loading notification that updates
const id = notifications.show({
loading: true,
title: "Uploading file",
message: "Please wait...",
autoClose: false,
withCloseButton: false,
});
// Update to success
notifications.update({
id,
loading: false,
title: "Upload complete",
message: "File has been uploaded",
color: "green",
autoClose: 3000,
});
// Clean all
notifications.clean();
Why good: Queue management prevents notification flooding, update pattern handles async operations elegantly, consistent positioning across the app
Pattern 10: @mantine/dates
Date components built on dayjs. Requires separate package and CSS import.
npm install @mantine/dates dayjs
import "@mantine/dates/styles.css";
import { DatePickerInput, DatesProvider } from "@mantine/dates";
// Wrap with DatesProvider for locale/firstDayOfWeek
<DatesProvider settings={{ locale: "en", firstDayOfWeek: 0 }}>
<App />
</DatesProvider>;
// v7: Date objects
const [date, setDate] = useState<Date | null>(null);
// v8: string values in "YYYY-MM-DD" format
// const [date, setDate] = useState<string | null>(null);
<DatePickerInput
label="Pick date"
placeholder="Pick date"
value={date}
onChange={setDate}
/>;
// v7: Date range
const [range, setRange] = useState<[Date | null, Date | null]>([null, null]);
// v8: const [range, setRange] = useState<[string | null, string | null]>([null, null]);
<DatePickerInput
type="range"
label="Date range"
value={range}
onChange={setRange}
/>;
Why good: dayjs is lightweight (2KB), DatesProvider centralizes locale settings, type parameter controls single/range/multiple selection modes
v8 note: Mantine v8 changed all
@mantine/datescomponents fromDateobjects to"YYYY-MM-DD"strings.DatesProviderno longer supports thetimezoneoption.
Detailed Resources:
- examples/core.md - MantineProvider setup, CSS Modules patterns, Styles API, style props, PostCSS mixins
- examples/theming.md - Custom colors, component defaults, virtualColor, theme extension
- examples/forms.md - useForm validation, Zod resolver, nested fields, dynamic lists
- reference.md - Decision frameworks, component selection, anti-patterns, quick reference
<red_flags>
RED FLAGS
High Priority Issues:
- Using
createStyles- Removed in v7. Use CSS Modules (.module.css) instead - Missing PostCSS preset - Responsive mixins (
@mixin smaller-than),light-dark(), andrem()will not work - Missing
@mantine/core/styles.cssimport - Components will render unstyled - Providing fewer than 10 shades for custom colors - Mantine requires exactly 10-shade tuples, TypeScript will error
Medium Priority Issues:
- Using
colorSchemedirectly for toggle logic - Returns"auto"when set to auto; useuseComputedColorSchemeto get resolved"light"or"dark" - Forgetting
form.key()in uncontrolled mode - Required for React key stability when usingmode: "uncontrolled"withgetInputProps - Not importing package-specific CSS -
@mantine/dates/styles.css,@mantine/notifications/styles.cssmust be imported separately if using those packages - Overriding styles with inline
styleprop - UseclassNames/stylesStyles API or CSS Modules to target inner elements properly
Gotchas & Edge Cases:
mantineHtmlPropsrequired for SSR - Must be spread on<html>element to prevent hydration mismatchesColorSchemeScriptmust be in<head>- Prevents flash of wrong color scheme before hydration- Style props use theme values, not CSS values -
p="md"resolves totheme.spacing.md, not the string"md" - Color dot notation -
color="blue.6"accesses shade 6 of the blue palette, not a CSS color name useMediaQueryreturnsundefinedduring SSR - Handle the initial render state to prevent hydration mismatch- PostCSS breakpoint variables must match Mantine defaults - If you change
theme.breakpoints, updatepostcss.config.cjsvariables too - v8 exists (released May 2025) - Key v8 changes:
@mantine/datesuses"YYYY-MM-DD"strings instead ofDateobjects,DatesProviderdropstimezone,CodeHighlightdrops highlight.js. Check migration guide if upgrading light-dark()is a PostCSS function, not native CSS - Requirespostcss-preset-mantineto compile
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST wrap your app in MantineProvider and import @mantine/core/styles.css at the root)
(You MUST install and configure postcss-preset-mantine for CSS Modules mixins and responsive utilities)
(You MUST use CSS Modules (.module.css) for custom component styles - createStyles was removed in v7)
(You MUST provide 10-shade color tuples when adding custom colors to the theme)
Failure to follow these rules will cause unstyled components, broken responsive layouts, and TypeScript errors.
</critical_reminders>
More from agents-inc/skills
web-animation-css-animations
CSS Animation patterns - transitions, keyframes, scroll-driven animations, @property, GPU-accelerated properties, accessibility with prefers-reduced-motion
20web-testing-playwright-e2e
Playwright E2E testing patterns - test structure, Page Object Model, locator strategies, assertions, network mocking, visual regression, parallel execution, fixtures, and configuration
18web-animation-view-transitions
View Transitions API patterns - same-document transitions, cross-document MPA transitions, shared element animations, pseudo-element styling, accessibility
17web-animation-framer-motion
Motion (formerly Framer Motion) animation patterns - motion components, variants, gestures, layout animations, scroll-linked animations, accessibility
17web-styling-cva
Class Variance Authority - type-safe component variant styling with cva(), compound variants, and VariantProps
16web-i18n-next-intl
Type-safe i18n for Next.js App Router
16