
Mantine Dev
- 267 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Scaffold dashboards, forms, and data tables faster with Mantine components, theming, hooks, and notifications in React SaaS and internal admin interfaces.
About
Covers building React interfaces with the Mantine component library, including theming, form validation, data tables, modals, and notifications for SaaS dashboards and internal admin tools.
- Mantine React component library patterns
- Theming and dark-mode setup
- Form validation with Mantine hooks
- Data tables and dashboard layouts
- Notifications and modal workflows
Mantine Dev by the numbers
- 267 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #780 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill mantine-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 267 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Scaffold dashboards, forms, and data tables faster with Mantine components, theming, hooks, and notifications in React SaaS and internal admin interfaces.
Files
Mantine UI Library
Mantine is a fully-featured React components library with TypeScript support. It provides 100+ hooks and components with native dark mode, CSS-in-JS via CSS modules, and excellent accessibility.
v9.3 Highlights
- New
Splittercomponent (withuse-splitterhook): declarative resizable split-pane layout with horizontal/vertical orientation, collapsible panes, and keyboard navigation.Splitter.Panemust be a direct child ofSplitter. - New component props:
Paginationgainslayout="responsive"(CSS container queries),Text/BlockquotegaintextWrap,CodeHighlightgainswithLineNumbers,OverflowListgainscollapseFrom("start"/"end"),TextareagainsbottomSection(character counters), and the Combobox family (Select,MultiSelect,Autocomplete,TagsInput) supportsfloatingHeight="viewport". DateInputgainspresetsfor quick value selection;Menu.Subsupports controlled open state.9.3.1patch fixes: notification DOM cleanup on close,CollapsekeepMounted,SegmentedControlindicator radius,PinInputplaceholder centering,Treearrow-key navigation skipping hidden nodes,MaskInputwith uncontrolleduse-form, and stableuse-idunder anActivitywrapper.
v9.2.2 Highlights
- Accessibility fixes landed across selection controls and sliders:
Checkbox,Radio, andSwitchnow wire error ids intoaria-describedby, whileSliderandRangeSlidersupportaria-valuetext. - Overlay/input behavior was refined with submenu safe polygon support, duplicate modal-id fixes, PinInput keyboard fixes, and ScrollArea/TreeSelect interaction fixes.
- Patch fixes in
9.2.2coverPilloverflow,Inputsections under localdiroverrides,Selectclear buttons for falsy primitive values, Modal/Drawer/Spotlight attribute types, and saferMenu.Subsafe-area polygon option forwarding. @mantine/hooksuse-masknow preserves undo and cursor position across paste/cut flows, and@mantine/tiptapcontrols avoid errors after the editor is destroyed or not initialized.- Forms and theming were hardened:
@mantine/formhandlers are more stable, default validators no longer force async results unexpectedly, andmergeMantineThemeno longer mutatesDEFAULT_THEME.headings. @mantine/schedulecontinues to mature with better multi-day overlap rendering and corrected positioning forintervalMinutes={60}.
v9.0 Breaking Changes
- React 19.2+ required for all
@mantine/*packages - Tiptap 3+ required for
@mantine/tiptap - Recharts 3+ required for
@mantine/charts(no migration needed) - Follow the official 8.x → 9.x migration guide for full details
Focus
This skill focuses on:
- Vite + TypeScript setup (not Next.js or CRA)
- CSS modules with PostCSS preset
- Vitest for testing
- ESLint with eslint-config-mantine
Installation
See references/getting-started.md for Vite template setup, manual installation, and optional packages.
PostCSS Configuration
Create 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",
},
},
},
};App Setup
// src/App.tsx
import "@mantine/core/styles.css";
// Other style imports as needed:
// import '@mantine/dates/styles.css';
// import '@mantine/notifications/styles.css';
import { MantineProvider, createTheme } from "@mantine/core";
const theme = createTheme({
// Theme customization here
});
function App() {
return <MantineProvider theme={theme}>{/* Your app */}</MantineProvider>;
}Critical Prohibitions
- Do NOT skip MantineProvider wrapper — all components require it
- Do NOT forget to import
@mantine/core/styles.css— components won't style without it - Do NOT mix Mantine with other UI libraries (e.g., Chakra, MUI) in same project
- Do NOT use inline styles for theme values — use CSS variables or theme object
- Do NOT skip PostCSS setup — responsive mixins won't work
- Do NOT forget
key={form.key('path')}when using uncontrolled forms
Core Concepts
1. MantineProvider
Wraps your app, provides theme context and color scheme management.
2. Theme Object
Customize colors, typography, spacing, component default props.
3. Style Props
All components accept style props like mt, p, c, bg, etc.
4. CSS Variables
All theme values exposed as CSS variables (e.g., --mantine-color-blue-6).
5. Polymorphic Components
Many components support component prop to render as different elements.
Definition of Done
- [ ] MantineProvider wraps the app
- [ ] Styles imported (
@mantine/core/styles.css) - [ ] PostCSS configured with mantine-preset
- [ ] Theme customization in createTheme
- [ ] Color scheme (light/dark) handled
- [ ] TypeScript types working
- [ ] Tests pass with Vitest + custom render
References (Detailed Guides)
Setup & Configuration
- getting-started.md — Installation, Vite setup, project structure
- styling.md — MantineProvider, theme, CSS modules, style props, dark mode
Core Features
- components.md — Core UI components patterns
- hooks.md — @mantine/hooks utility hooks
- forms.md — @mantine/form, useForm, validation
- schedule.md — @mantine/schedule calendar scheduling
Development
- testing.md — Vitest setup, custom render, mocking
- eslint.md — eslint-config-mantine setup
Links
Components Reference
@mantine/core provides 120+ components. This reference covers key patterns.
New in v9.3
Splitter
Resizable split-pane layout built on the use-splitter hook. Splitter.Pane must be a direct child of Splitter; nest Splitter for complex layouts.
import { Splitter } from "@mantine/core";
<Splitter h={200}>
<Splitter.Pane defaultSize={50} min={20}>First pane</Splitter.Pane>
<Splitter.Pane defaultSize={50} min={20}>Second pane</Splitter.Pane>
</Splitter>;Splitter props: orientation ("horizontal" default / "vertical"), sizes + onSizeChange (controlled), splitterRef (imperative API), lineSize, withHandle, redistribute. Splitter.Pane props: defaultSize, min, collapsible, plus standard style props.
New props on existing components
Paginationlayout="responsive"uses CSS container queries to adapt to available width.TextandBlockquotegaintextWrapto control line length.CodeHighlightgainswithLineNumbers.OverflowListgainscollapseFrom("start" / "end") to control collapse direction.TextareagainsbottomSectionfor character counters and supplementary info.- Combobox family (
Select,MultiSelect,Autocomplete,TagsInput) supportsfloatingHeight="viewport". DateInputgainspresetsfor quick value selection;Menu.Subsupports controlled open state.
Patch notes (v9.2.1 -> v9.2.2)
Checkbox,Radio, andSwitchnow pass the generated error id intoaria-describedby, which matters if you depend on Mantine's built-in error text for screen-reader context.SliderandRangeSlidersupportaria-valuetext; use it when the numeric value needs a spoken label different from the raw number.Menuadds safe polygon support for submenus, reducing accidental submenu closure on diagonal pointer movement.Menu.Subcan receive safe-area polygon options; use this when submenu pointer corridors need custom tuning.Tablefixes sticky-header border rendering,PinInputstops blocking common numeric-input keyboard shortcuts, andHighlightsupports accent-insensitive matching.Pilloverflow handling,Inputsection placement under localdiroverrides, andSelectclear-button visibility for falsy primitive values were fixed in9.2.2; remove local layout/clear-button workarounds after upgrade.- Modal, Drawer, and Spotlight attribute typings were corrected; re-run TypeScript on wrappers that pass custom attributes through those components.
@mantine/tiptapcontrols no longer throw when the editor is destroyed or not initialized, but still guard custom controls against null editor instances in app code.Dropzonenow defaultsuseFsAccessApitofalsefor broader browser compatibility; enable it explicitly only when you need the File System Access API behavior and your target browsers support it.
New in v9.0
FloatingWindow
Draggable floating element with viewport constraint support:
import { FloatingWindow } from "@mantine/core";
<FloatingWindow w={280} p="md" withBorder radius="md" excludeDragHandleSelector="button" initialPosition={{ top: 300, left: 20 }} style={{ cursor: "move" }}>
Drag me around
</FloatingWindow>;OverflowList
Displays items and collapses overflowing ones into a single element.
Typography
Generalized text component.
Collapse (horizontal)
Collapse now supports orientation="horizontal" for animating width.
Layout Components
Container, Stack, Group, Flex
import { Container, Stack, Group, Flex } from '@mantine/core';
<Container size="md">{/* Centers content, max-width */}</Container>
<Stack gap="md">{/* Vertical flex */}</Stack>
<Group gap="sm" justify="space-between">{/* Horizontal flex */}</Group>
<Flex direction="column" gap="md" align="center">{/* Generic flex */}</Flex>Grid & SimpleGrid
import { Grid, SimpleGrid } from '@mantine/core';
// CSS Grid with responsive spans
<Grid>
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }}>Responsive</Grid.Col>
</Grid>
// Equal-width columns
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>{/* Items */}</SimpleGrid>Button Variants
import { Button, ActionIcon } from '@mantine/core';
<Button variant="filled">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="light">Light</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="white">White</Button>
<Button loading>Loading state</Button>
<Button leftSection={<IconPlus />}>With Icon</Button>
// Icon button
<ActionIcon variant="filled" color="blue"><IconSettings /></ActionIcon>Inputs Pattern
All inputs follow consistent API:
import { TextInput, PasswordInput, Textarea, NumberInput, Select } from '@mantine/core';
// Common props: label, description, error, required, placeholder
<TextInput
label="Email"
description="We won't share it"
error="Invalid email"
required
withAsterisk
/>
<Select
label="Country"
data={['USA', 'Canada']}
searchable
clearable
/>
// Objects with value/label
<Select data={[{ value: 'us', label: 'United States' }]} />Overlays Pattern
Modals, Drawers, Menus, Popovers all use similar pattern:
import { Modal, Drawer, Menu, Popover } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
// Common pattern with useDisclosure
const [opened, { open, close }] = useDisclosure(false);
// Modal
<Modal opened={opened} onClose={close} title="Title">Content</Modal>
// Drawer
<Drawer opened={opened} onClose={close} position="left">Navigation</Drawer>
// Menu (dropdown)
<Menu>
<Menu.Target><Button>Toggle</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconSettings />}>Settings</Menu.Item>
<Menu.Divider />
<Menu.Item color="red">Delete</Menu.Item>
</Menu.Dropdown>
</Menu>
// Popover
<Popover width={200} withArrow>
<Popover.Target><Button>Info</Button></Popover.Target>
<Popover.Dropdown>Details here</Popover.Dropdown>
</Popover>Feedback Components
import { Loader, Alert, Notification, Progress, Skeleton } from '@mantine/core';
<Loader type="bars" /> // oval, bars, dots
<Alert variant="light" color="blue" title="Info">Message</Alert>
<Notification title="Success" color="green" icon={<IconCheck />}>
Saved!
</Notification>
<Progress value={65} />
<Progress.Root size="xl">
<Progress.Section value={35} color="cyan"><Progress.Label>Docs</Progress.Label></Progress.Section>
</Progress.Root>
// Loading placeholders
<Skeleton height={50} circle />
<Skeleton height={8} radius="xl" />Typography
import { Title, Text, Anchor, Highlight, Code } from '@mantine/core';
<Title order={1}>h1 heading</Title>
<Title order={2} c="dimmed">h2 dimmed</Title>
<Text size="sm" c="dimmed" fw={700}>Small bold dimmed</Text>
<Text truncate>Long text...</Text>
<Text lineClamp={2}>Multi-line truncate...</Text>
<Highlight highlight={['react', 'mantine']}>
Learn React with Mantine
</Highlight>
<Code>inline</Code>
<Code block>{`const x = 1;`}</Code>Data Display
import { Badge, Card, Table, Avatar, Image, Tabs, Accordion } from '@mantine/core';
// Badge variants
<Badge>Default</Badge>
<Badge variant="dot" color="red">Dot</Badge>
// Card with sections
<Card shadow="sm" padding="lg" withBorder>
<Card.Section><Image src="/img.jpg" height={160} /></Card.Section>
<Text>Content</Text>
</Card>
// Table
<Table striped highlightOnHover withTableBorder>
<Table.Thead><Table.Tr><Table.Th>Name</Table.Th></Table.Tr></Table.Thead>
<Table.Tbody><Table.Tr><Table.Td>John</Table.Td></Table.Tr></Table.Tbody>
</Table>
// Tabs
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">First</Tabs.Tab>
<Tabs.Tab value="tab2">Second</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="tab1">Content 1</Tabs.Panel>
</Tabs>
// Accordion
<Accordion defaultValue="item-1">
<Accordion.Item value="item-1">
<Accordion.Control>Section 1</Accordion.Control>
<Accordion.Panel>Content</Accordion.Panel>
</Accordion.Item>
</Accordion>Navigation
import { NavLink, Pagination, Stepper, Breadcrumbs } from '@mantine/core';
<NavLink href="#" label="Dashboard" leftSection={<IconHome />} active />
<NavLink label="Settings">
<NavLink label="General" />
<NavLink label="Security" />
</NavLink>
<Pagination total={10} value={page} onChange={setPage} />
<Stepper active={active}>
<Stepper.Step label="Step 1">Content 1</Stepper.Step>
<Stepper.Step label="Step 2">Content 2</Stepper.Step>
<Stepper.Completed>Done!</Stepper.Completed>
</Stepper>Common Style Props
All components accept these props:
<Component
// Margin & Padding
m="md"
mt="xs"
p="sm"
px="md"
// Colors
c="dimmed"
bg="blue.1"
// Typography
fw={500}
fz="sm"
// Dimensions
w={200}
h="100%"
maw={500}
// Responsive
p={{ base: "xs", sm: "md", lg: "xl" }}
/>Polymorphic Components
Render as different elements:
import { Button } from '@mantine/core';
import { Link } from 'react-router-dom';
<Button component={Link} to="/about">Link Button</Button>
<Button component="a" href="https://example.com">Anchor Button</Button>Visibility Props
<Text hiddenFrom="sm">Hidden on sm+</Text>
<Text visibleFrom="md">Visible on md+</Text>
<Text lightHidden>Only in dark mode</Text>
<Text darkHidden>Only in light mode</Text>ESLint Configuration Reference
eslint-config-mantine provides ESLint rules and configurations used in Mantine projects.
Installation
npm install -D @eslint/js eslint eslint-plugin-jsx-a11y eslint-plugin-react typescript-eslint eslint-config-mantineConfiguration
Create eslint.config.js (ESLint flat config):
import mantine from 'eslint-config-mantine';
import tseslint from 'typescript-eslint';
export default [
...tseslint.configs.recommended,
...mantine,
{
ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}'],
},
{
files: ['**/*.story.tsx'],
rules: {
'no-console': 'off',
},
},
{
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: process.cwd(),
},
},
},
];What's Included
eslint-config-mantine includes:
TypeScript Rules
typescript-eslint/recommendedbase rules- Strict type checking
- No unused variables (except with
_prefix) - No explicit
any(warning)
React Rules
- React hooks rules (exhaustive-deps, rules-of-hooks)
- JSX-specific rules
- No unknown properties
- Self-closing components
Accessibility (a11y)
eslint-plugin-jsx-a11yrules- Alt text requirements
- ARIA attribute validation
- Interactive element handling
- Focus management rules
Import/Export
- Import order organization
- No duplicate imports
- No unresolved imports
General
- No console.log (warning)
- Consistent code style
- No debugger
Script Setup
Add to package.json:
{
"scripts": {
"lint": "npm run eslint && npm run stylelint",
"eslint": "eslint . --cache",
"eslint:fix": "eslint . --cache --fix"
}
}Common Configuration Adjustments
Allow console in specific files
export default [
...mantine,
{
files: ['**/*.test.tsx', '**/*.story.tsx'],
rules: {
'no-console': 'off',
},
},
];Disable specific rules
export default [
...mantine,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'react/jsx-no-target-blank': 'off',
},
},
];Add custom rules
export default [
...mantine,
{
rules: {
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
},
},
];Configure for monorepo
export default [
...mantine,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json', './packages/*/tsconfig.json'],
tsconfigRootDir: process.cwd(),
},
},
},
];Integration with Prettier
If using Prettier, add eslint-config-prettier:
npm install -D eslint-config-prettierimport mantine from 'eslint-config-mantine';
import prettier from 'eslint-config-prettier';
export default [
...mantine,
prettier, // Must be last to override conflicting rules
];VS Code Integration
Install ESLint extension, then add to .vscode/settings.json:
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}Stylelint (Optional)
For CSS linting, the Mantine Vite template also includes Stylelint:
npm install -D stylelint stylelint-config-standard-scssCreate stylelint.config.js:
export default {
extends: ['stylelint-config-standard-scss'],
rules: {
'selector-class-pattern': null,
},
};Add to scripts:
{
"scripts": {
"stylelint": "stylelint '**/*.css' --cache"
}
}Common ESLint Errors & Fixes
React Hook Dependency Warning
// Warning: React Hook useEffect has missing dependency
useEffect(() => {
fetchData(id);
}, []); // Missing 'id' and 'fetchData'
// Fix: Add dependencies
useEffect(() => {
fetchData(id);
}, [id, fetchData]);
// Or use useCallback for functions
const fetchData = useCallback((id) => { /* ... */ }, []);Unused Variable
// Error: 'x' is defined but never used
const x = 5;
// Fix: Prefix with underscore if intentionally unused
const _x = 5;Missing Key Prop
// Error: Missing "key" prop
items.map((item) => <Item>{item.name}</Item>);
// Fix: Add unique key
items.map((item) => <Item key={item.id}>{item.name}</Item>);Accessibility Issues
// Error: img elements must have an alt prop
<img src="photo.jpg" />
// Fix: Add alt text
<img src="photo.jpg" alt="Description" />
// Decorative image
<img src="decoration.jpg" alt="" role="presentation" />Template Configuration
The Mantine Vite template includes a complete ESLint + Prettier + Stylelint setup that you can use as reference.
Forms Reference
@mantine/form provides useForm hook for managing form state, validation, and submission.
Patch notes (v9.2.1)
- Some form handlers were stabilized again in the
9.2.1line. If you memoize or compare handler references in advanced integrations, re-test before keeping old workarounds. - Default validators no longer force
form.validate()into an async-looking path unexpectedly; treat sync validation as sync again unless your own schema/validator layer is async.
Installation
npm install @mantine/formNo styles needed — works with or without @mantine/core.
Basic Usage
import { useForm } from "@mantine/form";
import { TextInput, Button, Box } from "@mantine/core";
interface FormValues {
email: string;
name: string;
}
function Demo() {
const form = useForm<FormValues>({
mode: "uncontrolled", // Recommended for performance
initialValues: {
email: "",
name: "",
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"),
name: (value) => (value.length < 2 ? "Name too short" : null),
},
});
return (
<Box component="form" onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput label="Name" placeholder="Your name" key={form.key("name")} {...form.getInputProps("name")} />
<TextInput label="Email" placeholder="your@email.com" key={form.key("email")} {...form.getInputProps("email")} />
<Button type="submit" mt="md">
Submit
</Button>
</Box>
);
}useForm Options
interface UseFormInput<Values> {
mode?: "controlled" | "uncontrolled"; // Default: 'controlled'
initialValues?: Values;
initialErrors?: FormErrors;
initialDirty?: Record<string, boolean>;
initialTouched?: Record<string, boolean>;
validate?: FormValidation<Values>;
validateInputOnChange?: boolean | string[];
validateInputOnBlur?: boolean | string[];
clearInputErrorOnChange?: boolean;
onValuesChange?: (values: Values, previous: Values) => void;
onSubmitPreventDefault?: "always" | "never" | "validation-failed";
}Controlled vs Uncontrolled Mode
Uncontrolled (Recommended)
Better performance — values stored in DOM:
const form = useForm({
mode: "uncontrolled", // Add mode
initialValues: { name: "" },
});
<TextInput
key={form.key("name")} // Required for uncontrolled
{...form.getInputProps("name")}
/>;Controlled
Values stored in React state — re-renders on every change:
const form = useForm({
mode: "controlled",
initialValues: { name: "" },
});
<TextInput {...form.getInputProps("name")} />;Form Values
// Get all values
const values = form.getValues();
// Set single field
form.setFieldValue("email", "new@email.com");
// Set multiple values
form.setValues({ name: "John", email: "john@email.com" });
// Set values from previous state
form.setValues((prev) => ({ ...prev, name: "Updated" }));
// Reset to initialValues
form.reset();
// Reset single field
form.resetField("email");
// Update initialValues (affects reset)
form.setInitialValues({ name: "New Initial" });Validation
Inline Rules
const form = useForm({
mode: "uncontrolled",
initialValues: {
email: "",
age: 0,
password: "",
confirmPassword: "",
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"),
age: (value) => (value < 18 ? "Must be 18+" : null),
// Access all values for cross-field validation
confirmPassword: (value, values) => (value !== values.password ? "Passwords do not match" : null),
},
});Function-based Validation
const form = useForm({
mode: "uncontrolled",
initialValues: { name: "", email: "" },
validate: (values) => ({
name: values.name.length < 2 ? "Name too short" : null,
email: !values.email.includes("@") ? "Invalid email" : null,
}),
});Schema Validation (Zod, Yup, Joi)
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2, "Name must have at least 2 characters"),
email: z.string().email("Invalid email"),
age: z.number().min(18, "Must be 18 or older"),
});
const form = useForm({
mode: "uncontrolled",
initialValues: { name: "", email: "", age: 0 },
validate: zodResolver(schema),
});Install resolver:
npm install mantine-form-zod-resolver zod
# or
npm install mantine-form-yup-resolver yup
# or
npm install mantine-form-joi-resolver joiValidation Timing
const form = useForm({
mode: "uncontrolled",
validateInputOnChange: true, // Validate all on change
validateInputOnBlur: true, // Validate all on blur
clearInputErrorOnChange: true, // Clear error when value changes (default)
});
// Validate specific fields only
const form = useForm({
mode: "uncontrolled",
validateInputOnChange: ["email", "password"],
validateInputOnBlur: ["email"],
});Manual Validation
// Validate all fields
const result = form.validate();
// result.hasErrors: boolean
// result.errors: FormErrors
// Validate single field
form.validateField("email");
// Check if valid (without setting errors)
const isValid = form.isValid();
const isEmailValid = form.isValid("email");Errors
// Get current errors
form.errors; // { email: 'Invalid', name: null }
// Set error
form.setFieldError("email", "This email is taken");
// Set multiple errors
form.setErrors({ email: "Invalid", name: "Required" });
// Clear all errors
form.clearErrors();
// Clear single field error
form.clearFieldError("email");Form Submission
<form
onSubmit={form.onSubmit(
// Success handler - called when validation passes
(values, event) => {
console.log("Valid:", values);
// Submit to API
},
// Error handler - called when validation fails
(errors, values, event) => {
console.log("Errors:", errors);
// Show notification, focus first error, etc.
},
)}
>
{/* inputs */}
</form>Nested Objects
const form = useForm({
mode: 'uncontrolled',
initialValues: {
user: {
firstName: '',
lastName: '',
address: {
city: '',
country: '',
},
},
},
validate: {
user: {
firstName: (value) => (value.length < 2 ? 'Too short' : null),
address: {
city: (value) => (!value ? 'Required' : null),
},
},
},
});
<TextInput
key={form.key('user.firstName')}
{...form.getInputProps('user.firstName')}
/>
<TextInput
key={form.key('user.address.city')}
{...form.getInputProps('user.address.city')}
/>List Fields
const form = useForm({
mode: "uncontrolled",
initialValues: {
employees: [{ name: "", email: "" }],
},
});
// Add item
form.insertListItem("employees", { name: "", email: "" });
// Add at specific index
form.insertListItem("employees", { name: "", email: "" }, 0);
// Remove item
form.removeListItem("employees", 1);
// Replace item
form.replaceListItem("employees", 0, { name: "New", email: "new@email.com" });
// Reorder items
form.reorderListItem("employees", { from: 0, to: 2 });Rendering List
import { FORM_INDEX } from "@mantine/form";
function Demo() {
const fields = form.getValues().employees.map((item, index) => (
<Group key={item.key}>
<TextInput key={form.key(`employees.${index}.name`)} {...form.getInputProps(`employees.${index}.name`)} />
<TextInput key={form.key(`employees.${index}.email`)} {...form.getInputProps(`employees.${index}.email`)} />
<ActionIcon onClick={() => form.removeListItem("employees", index)}>
<IconTrash />
</ActionIcon>
</Group>
));
return (
<Box>
{fields}
<Button onClick={() => form.insertListItem("employees", { name: "", email: "" })}>Add Employee</Button>
</Box>
);
}
// Validation for list items with FORM_INDEX
const form = useForm({
mode: "uncontrolled",
validateInputOnChange: [`employees.${FORM_INDEX}.name`],
});Touched & Dirty State
// Check if any field was interacted with
form.isTouched();
form.isTouched("email");
// Check if values differ from initialValues
form.isDirty();
form.isDirty("email");
// Set touched state
form.setTouched({ email: true, name: false });
form.resetTouched();
// Set dirty state
form.setDirty({ email: true });
form.resetDirty(); // Snapshot current values as "clean"Form Context
Share form across components without prop drilling:
// formContext.ts
import { createFormContext } from "@mantine/form";
interface FormValues {
name: string;
email: string;
}
export const [FormProvider, useFormContext, useForm] = createFormContext<FormValues>();// Parent component
import { FormProvider, useForm } from "./formContext";
function Parent() {
const form = useForm({
mode: "uncontrolled",
initialValues: { name: "", email: "" },
});
return (
<FormProvider form={form}>
<NameInput />
<EmailInput />
</FormProvider>
);
}
// Child component
import { useFormContext } from "./formContext";
function NameInput() {
const form = useFormContext();
return <TextInput key={form.key("name")} {...form.getInputProps("name")} />;
}UseFormReturnType
Type for passing form as prop:
import { UseFormReturnType } from "@mantine/form";
interface Props {
form: UseFormReturnType<{ name: string; email: string }>;
}
function NameField({ form }: Props) {
return <TextInput key={form.key("name")} {...form.getInputProps("name")} />;
}Built-in Validators
import { isNotEmpty, isEmail, hasLength, matches, isInRange } from "@mantine/form";
const form = useForm({
mode: "uncontrolled",
initialValues: {
name: "",
email: "",
age: 0,
website: "",
terms: false,
},
validate: {
name: isNotEmpty("Name is required"),
email: isEmail("Invalid email"),
age: isInRange({ min: 18, max: 99 }, "Age must be 18-99"),
website: matches(/^https?:\/\//, "Must start with http"),
terms: isNotEmpty("Must accept terms"),
},
});Focus First Error
<form
onSubmit={form.onSubmit(
(values) => { /* success */ },
(errors) => {
const firstErrorPath = Object.keys(errors)[0];
form.getInputNode(firstErrorPath)?.focus();
}
)}
>Getting Started Reference
Vite Template (Recommended)
The fastest way to start — official template includes everything:
# Clone template
git clone https://github.com/mantinedev/vite-template my-app
cd my-app
# Install dependencies
yarn install # or npm install
# Start development
yarn devTemplate Features
- PostCSS with
postcss-preset-mantine - TypeScript configured
- Storybook setup
- Vitest with React Testing Library
- ESLint with
eslint-config-mantine - Prettier configured
Manual Setup
1. Create Vite Project
npm create vite@latest my-app -- --template react-ts
cd my-app2. Install Packages
# Core (required)
npm install @mantine/core @mantine/hooks
# PostCSS (required for responsive styles)
npm install -D postcss postcss-preset-mantine postcss-simple-vars
# Optional packages
npm install @mantine/form # Forms with validation
npm install @mantine/dates dayjs # Date/time components
npm install @mantine/notifications # Toast notifications
npm install @mantine/modals # Modal manager
npm install @mantine/charts recharts # Charts
npm install @mantine/dropzone # File upload
npm install @mantine/spotlight # Command palette (Cmd+K)
npm install @mantine/code-highlight # Code syntax highlighting
npm install @mantine/carousel embla-carousel-react # Carousel
npm install @mantine/tiptap @tiptap/react @tiptap/pm @tiptap/starter-kit # Rich text editor3. Configure PostCSS
Create 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',
},
},
},
};4. Import Styles
In your entry file (e.g., src/main.tsx or src/App.tsx):
// Required - core styles
import '@mantine/core/styles.css';
// Package-specific styles (import only what you use)
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/code-highlight/styles.css';
import '@mantine/dropzone/styles.css';
import '@mantine/spotlight/styles.css';
import '@mantine/carousel/styles.css';
import '@mantine/tiptap/styles.css';
// Note: @mantine/form and @mantine/hooks have no styles5. Setup MantineProvider
// src/App.tsx
import '@mantine/core/styles.css';
import { MantineProvider, createTheme } from '@mantine/core';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const theme = createTheme({
// Your theme customization
primaryColor: 'blue',
fontFamily: 'Inter, sans-serif',
});
function App() {
return (
<MantineProvider theme={theme}>
<BrowserRouter>
<Routes>
{/* Your routes */}
</Routes>
</BrowserRouter>
</MantineProvider>
);
}
export default App;Project Structure
Recommended structure for Mantine projects:
src/
├── components/
│ ├── Layout/
│ │ ├── AppShell.tsx
│ │ ├── Header.tsx
│ │ └── Navbar.tsx
│ └── ui/ # Custom UI components
├── pages/
│ ├── Home.tsx
│ └── Settings.tsx
├── hooks/ # Custom hooks
├── theme/
│ ├── index.ts # createTheme export
│ └── components.ts # Component default props
├── utils/
├── App.tsx
├── main.tsx
└── App.module.css # CSS modulesVS Code Setup
Install recommended extensions:
1. PostCSS Intellisense and Highlighting For postcss syntax support and $variable recognition.
2. CSS Variable Autocomplete For Mantine CSS variables autocomplete.
Create .vscode/settings.json:
{
"cssVariables.lookupFiles": [
"**/*.css",
"node_modules/@mantine/core/styles.css"
]
}TypeScript Configuration
Mantine is fully typed. Your tsconfig.json should have:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"skipLibCheck": true
}
}Common Issues
Styles Not Loading
Ensure @mantine/core/styles.css is imported before any component usage.
PostCSS Mixins Not Working
Check that postcss-preset-mantine is installed and configured in postcss.config.cjs.
Dark Mode Flash
For SSR apps, add ColorSchemeScript to <head>:
import { ColorSchemeScript } from '@mantine/core';
// In your HTML head
<ColorSchemeScript defaultColorScheme="auto" />Hydration Warning
Spread mantineHtmlProps on <html> element:
import { mantineHtmlProps } from '@mantine/core';
<html {...mantineHtmlProps}>Hooks Reference
@mantine/hooks provides 75+ React hooks. No styles needed — works independently.
npm install @mantine/hooksNew in v9.3
use-splitter
Powers the Splitter component: drag-based resizing of split panes with keyboard navigation. Use the hook directly when you need a custom resizable layout outside the Splitter component wrapper.
New in v9.0
useCollapse / useHorizontalCollapse
Hook versions of Collapse component for animating height/width from 0 to auto:
import { useCollapse, useDisclosure } from "@mantine/hooks";
const [expanded, handlers] = useDisclosure(false);
const getCollapseProps = useCollapse({ expanded });
<div {...getCollapseProps()}>Content</div>;useHorizontalCollapse works the same but animates width.
useFloatingWindow
Creates floating draggable elements with viewport constraints:
import { useFloatingWindow } from "@mantine/hooks";
const floatingWindow = useFloatingWindow({
constrainToViewport: true,
constrainOffset: 20,
excludeDragHandleSelector: "button",
initialPosition: { top: 300, left: 20 },
});
<div ref={floatingWindow.ref}>Draggable</div>;State Management
useDisclosure
Boolean state for modals/menus:
import { useDisclosure } from "@mantine/hooks";
const [opened, { open, close, toggle }] = useDisclosure(false);
// With callbacks
const [opened, handlers] = useDisclosure(false, {
onOpen: () => console.log("Opened"),
onClose: () => console.log("Closed"),
});useToggle
Cycle through values:
import { useToggle } from "@mantine/hooks";
const [value, toggle] = useToggle(["blue", "orange", "cyan"]);
// toggle() cycles, toggle('cyan') sets specific
// Boolean
const [active, toggle] = useToggle([false, true]);useCounter
Numeric counter with min/max:
import { useCounter } from "@mantine/hooks";
const [count, { increment, decrement, set, reset }] = useCounter(0, { min: 0, max: 10 });useListState
Array state management:
import { useListState } from "@mantine/hooks";
const [values, handlers] = useListState([1, 2, 3]);
handlers.append(4); // Add to end
handlers.prepend(0); // Add to start
handlers.insert(2, 99); // Insert at index
handlers.remove(1); // Remove at index
handlers.reorder({ from: 0, to: 2 }); // Move item
handlers.filter((item) => item > 2); // Filter
handlers.apply((item) => item * 2); // Transform alluseSetState
Object state with partial updates:
import { useSetState } from "@mantine/hooks";
const [state, setState] = useSetState({ name: "", email: "", age: 0 });
setState({ name: "John" }); // Partial update, others unchangedStorage
useLocalStorage / useSessionStorage
import { useLocalStorage } from "@mantine/hooks";
const [value, setValue, removeValue] = useLocalStorage({
key: "my-key",
defaultValue: "light",
});
// Auto-serializes objects
const [user, setUser] = useLocalStorage({
key: "user",
defaultValue: { name: "", preferences: {} },
});Input/Debounce
Patch note (9.2.2): use-mask fixes undo keyboard shortcuts and cursor jumping during paste/cut. Re-test masked phone/date/card inputs after upgrade before keeping custom cursor-preservation patches.
useDebouncedValue
import { useDebouncedValue } from "@mantine/hooks";
const [value, setValue] = useState("");
const [debounced] = useDebouncedValue(value, 300);
useEffect(() => {
// API call with debounced value
}, [debounced]);useDebouncedCallback
import { useDebouncedCallback } from "@mantine/hooks";
const search = useDebouncedCallback(async (query: string) => {
await searchAPI(query);
}, 300);
<TextInput onChange={(e) => search(e.target.value)} />;useInputState
Simpler input handling:
import { useInputState } from "@mantine/hooks";
const [name, setName] = useInputState("");
<TextInput value={name} onChange={setName} />;
const [checked, setChecked] = useInputState(false);
<Checkbox checked={checked} onChange={setChecked} />;UI Interactions
useClickOutside
import { useClickOutside } from "@mantine/hooks";
const ref = useClickOutside(() => close());
<Paper ref={ref}>Click outside to close</Paper>;useHover
import { useHover } from "@mantine/hooks";
const { hovered, ref } = useHover();
<Box ref={ref} bg={hovered ? "blue" : "gray"}>
Hover me
</Box>;useFocusWithin
import { useFocusWithin } from "@mantine/hooks";
const { ref, focused } = useFocusWithin();
<Box ref={ref} style={{ outline: focused ? "2px solid blue" : "none" }}>
<TextInput />
<TextInput />
</Box>;useMediaQuery
import { useMediaQuery } from "@mantine/hooks";
const isMobile = useMediaQuery("(max-width: 768px)");
const matches = useMediaQuery("(min-width: 48em)"); // sm breakpoint
return isMobile ? <MobileNav /> : <DesktopNav />;useViewportSize
import { useViewportSize } from "@mantine/hooks";
const { width, height } = useViewportSize();useElementSize
import { useElementSize } from "@mantine/hooks";
const { ref, width, height } = useElementSize();
<Box ref={ref}>Tracks this element's size</Box>;useScrollIntoView
import { useScrollIntoView } from '@mantine/hooks';
const { scrollIntoView, targetRef } = useScrollIntoView<HTMLDivElement>({
offset: 60,
duration: 500,
});
<Button onClick={() => scrollIntoView()}>Scroll</Button>
<div ref={targetRef}>Target</div>useIntersection / useInViewport
import { useIntersection, useInViewport } from "@mantine/hooks";
// Detailed intersection info
const { ref, entry } = useIntersection({ threshold: 0.5 });
// Simple visibility check
const { ref, inViewport } = useInViewport();Utilities
useClipboard
import { useClipboard } from "@mantine/hooks";
const clipboard = useClipboard({ timeout: 500 });
<Button onClick={() => clipboard.copy("Text")} color={clipboard.copied ? "teal" : "blue"}>
{clipboard.copied ? "Copied!" : "Copy"}
</Button>;useHotkeys
import { useHotkeys, getHotkeyHandler } from "@mantine/hooks";
useHotkeys([
["mod+S", () => save()],
["ctrl+K", () => search()],
]);
// On specific input
<input
onKeyDown={getHotkeyHandler([
["mod+Enter", submit],
["Escape", cancel],
])}
/>;useFullscreen
import { useFullscreen } from "@mantine/hooks";
const { toggle, fullscreen } = useFullscreen();
<Button onClick={toggle}>{fullscreen ? "Exit" : "Enter"} Fullscreen</Button>;useIdle
import { useIdle } from "@mantine/hooks";
const idle = useIdle(5000); // 5 seconds
<Text>{idle ? "User is idle" : "User is active"}</Text>;useInterval / useTimeout
import { useInterval, useTimeout } from "@mantine/hooks";
const interval = useInterval(() => tick(), 1000);
interval.start();
interval.stop();
interval.toggle();
const { start, clear } = useTimeout(() => action(), 3000);useDocumentTitle
import { useDocumentTitle } from "@mantine/hooks";
useDocumentTitle("My Page Title");useOs
import { useOs } from "@mantine/hooks";
const os = useOs(); // 'macos' | 'ios' | 'windows' | 'android' | 'linux'
<Text>Shortcut: {os === "macos" ? "⌘" : "Ctrl"}</Text>;useNetwork
import { useNetwork } from "@mantine/hooks";
const { online, downlink, effectiveType } = useNetwork();
return online ? <App /> : <OfflineMessage />;usePrevious
import { usePrevious } from "@mantine/hooks";
const [value, setValue] = useState(0);
const previous = usePrevious(value);useMergedRef
Combine multiple refs:
import { useMergedRef } from "@mantine/hooks";
const myRef = useRef<HTMLDivElement>(null);
const { ref: hookRef } = useHover();
const mergedRef = useMergedRef(myRef, hookRef);
<Box ref={mergedRef}>Content</Box>;Complete Hook List
State: useDisclosure, useToggle, useCounter, useListState, useSetState, useQueue, usePagination
Storage: useLocalStorage, useSessionStorage, readLocalStorageValue
Input: useDebouncedValue, useDebouncedState, useDebouncedCallback, useInputState, useValidatedState, useUncontrolled
UI: useClickOutside, useHover, useFocusWithin, useFocusReturn, useFocusTrap, useMediaQuery, useViewportSize, useElementSize, useResizeObserver, useScrollIntoView, useIntersection, useInViewport, useWindowScroll, useMouse, useMove
Utilities: useClipboard, useHotkeys, useFullscreen, useIdle, useInterval, useTimeout, useDocumentTitle, useDocumentVisibility, useFavicon, useOs, useNetwork, usePrevious, useMergedRef, useId, useForceUpdate, useReducedMotion, useTextSelection, useWindowEvent, useEventListener, useEyeDropper, useHash, useHeadroom, useLogger, useMutationObserver, useOrientation, usePageLeave, usePinchToZoom, useStateHistory
Schedule Reference
@mantine/schedule is new in Mantine 9.0. It provides calendar scheduling components with multiple view levels, drag-and-drop event management, and extensive customization.
Patch notes (v9.2.1)
MonthViewimproves multi-day event overlap rendering when a day is already visually saturated.- Event positioning with
intervalMinutes={60}was corrected; if you added manual offsets or custom CSS to compensate, re-test before keeping them.
Installation
npm install @mantine/scheduleImport styles in your app:
import "@mantine/schedule/styles.css";Core Components
Schedule
Unified container that combines all views with built-in navigation and view switching:
import { useState } from "react";
import { Schedule, ScheduleEventData } from "@mantine/schedule";
function Demo() {
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
const handleEventDrop = ({ eventId, newStart, newEnd }) => {
setEvents((prev) => prev.map((event) => (event.id === eventId ? { ...event, start: newStart, end: newEnd } : event)));
};
return <Schedule events={events} withEventsDragAndDrop onEventDrop={handleEventDrop} />;
}DayView
Single day with configurable time slots, all-day events, current time indicator, and business hours:
import { DayView } from "@mantine/schedule";
<DayView date={new Date()} events={events} startTime="08:00:00" endTime="18:00:00" withEventsDragAndDrop onEventDrop={handleEventDrop} />;WeekView
Weekly calendar grid with time slots, week numbers, weekend toggling, and multi-day spanning.
MonthView
Monthly calendar with event dots and configurable week start.
MobileMonthView
Touch-optimized month view for mobile UIs.
Event Data Shape
interface ScheduleEventData {
id: string | number;
title: string;
start: string; // 'YYYY-MM-DD HH:mm:ss'
end: string; // 'YYYY-MM-DD HH:mm:ss'
color?: string;
}Key Props
events— array ofScheduleEventDatawithEventsDragAndDrop— enable drag to rescheduleonEventDrop— callback with{ eventId, newStart, newEnd }startTime/endTime— visible time range (DayView/WeekView)
Styling & Theming Reference
Mantine styling: MantineProvider, theme object, CSS modules, style props, and Styles API.
Patch notes (v9.2.1)
mergeMantineThemeno longer mutatesDEFAULT_THEME.headings. If you merge themes dynamically, this reduces cross-test and cross-render leakage from shared default theme objects.
MantineProvider
Required wrapper for all Mantine components:
import { createTheme, MantineProvider } from "@mantine/core";
const theme = createTheme({
primaryColor: "blue",
fontFamily: "Inter, sans-serif",
});
function App() {
return <MantineProvider theme={theme}>{/* App content */}</MantineProvider>;
}Key Props
| Prop | Type | Default | Description |
|---|---|---|---|
theme | MantineThemeOverride | - | Theme customization |
defaultColorScheme | `'light' \ | 'dark' \ | 'auto'` |
forceColorScheme | `'light' \ | 'dark'` | - |
env | `'default' \ | 'test'` | 'default' |
Theme Object
import { createTheme, rem } from "@mantine/core";
const theme = createTheme({
// Colors
primaryColor: "blue",
primaryShade: { light: 6, dark: 8 },
// Typography
fontFamily: "Inter, sans-serif",
headings: {
fontFamily: "Greycliff CF, sans-serif",
fontWeight: "700",
},
// Spacing & Sizing
spacing: { xs: rem(10), sm: rem(12), md: rem(16), lg: rem(20), xl: rem(32) },
radius: { xs: rem(2), sm: rem(4), md: rem(8), lg: rem(16), xl: rem(32) },
// Defaults
defaultRadius: "md",
cursorType: "pointer",
respectReducedMotion: true,
});Custom Colors
import { createTheme, MantineColorsTuple } from "@mantine/core";
const brand: MantineColorsTuple = ["#f0f9ff", "#e0f2fe", "#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#0284c7", "#0369a1", "#075985", "#0c4a6e"];
const theme = createTheme({
colors: { brand },
primaryColor: "brand",
});Color Scheme (Dark Mode)
import { useMantineColorScheme, useComputedColorScheme } from "@mantine/core";
function ColorSchemeToggle() {
const { setColorScheme, toggleColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme("light"); // Resolved value
return (
<>
<Button onClick={() => setColorScheme("light")}>Light</Button>
<Button onClick={() => setColorScheme("dark")}>Dark</Button>
<Button onClick={toggleColorScheme}>Toggle</Button>
</>
);
}
// SSR: Prevent flash of wrong color scheme
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
<html {...mantineHtmlProps}>
<head>
<ColorSchemeScript defaultColorScheme="auto" />
</head>
</html>;Style Props
All components accept style props for quick styling:
import { Box, Text, Button } from "@mantine/core";
function Demo() {
return (
<Box
p="md" // padding
m="lg" // margin
mt="xl" // margin-top
bg="blue.6" // background (color.shade)
c="white" // color
w={200} // width (number = px)
h="100%" // height
maw={500} // max-width
pos="relative" // position
ta="center" // text-align
fz="sm" // font-size
fw={700} // font-weight
ff="monospace" // font-family
lh={1.5} // line-height
style={{ borderRadius: "var(--mantine-radius-md)" }}
>
<Text c="dimmed" fz="xs" tt="uppercase">
Uppercase dimmed text
</Text>
</Box>
);
}Common Style Props
| Prop | CSS Property | Example |
|---|---|---|
m, mx, my, mt, mr, mb, ml | margin | m="md", mt={20} |
p, px, py, pt, pr, pb, pl | padding | p="lg" |
w, h, maw, mah, miw, mih | width, height, max/min | w="100%" |
c | color | c="blue.6", c="dimmed" |
bg | background-color | bg="gray.1" |
fz | font-size | fz="sm", fz={14} |
fw | font-weight | fw={500}, fw="bold" |
ta | text-align | ta="center" |
td | text-decoration | td="underline" |
tt | text-transform | tt="uppercase" |
ff | font-family | ff="monospace" |
lh | line-height | lh={1.5} |
pos | position | pos="absolute" |
top, left, right, bottom | position offsets | top={10} |
display | display | display="flex" |
opacity | opacity | opacity={0.5} |
Responsive Props
<Box w={{ base: "100%", sm: "50%", md: 400 }} p={{ base: "xs", md: "xl" }} display={{ base: "none", md: "block" }}>
Responsive box
</Box>CSS Modules
Recommended styling approach. Create .module.css files:
/* Button.module.css */
.root {
background-color: var(--mantine-color-blue-6);
@mixin hover {
background-color: var(--mantine-color-blue-7);
}
/* Responsive */
@mixin smaller-than sm {
font-size: var(--mantine-font-size-xs);
}
@mixin larger-than md {
padding: var(--mantine-spacing-xl);
}
/* Dark mode */
@mixin dark {
background-color: var(--mantine-color-blue-8);
}
@mixin light {
background-color: var(--mantine-color-blue-4);
}
}
.label {
color: var(--mantine-color-white);
}import { Button } from "@mantine/core";
import classes from "./Button.module.css";
function Demo() {
return <Button classNames={classes}>Styled button</Button>;
}PostCSS Preset
postcss-preset-mantine provides:
Mixins
/* Hover state */
@mixin hover {
/* Hover-only styles */
}
/* Responsive breakpoints */
@mixin smaller-than sm {
}
@mixin larger-than md {
}
/* Color scheme */
@mixin light {
}
@mixin dark {
}
/* RTL support */
@mixin rtl {
}
@mixin ltr {
}Functions
.element {
/* rem() - convert to rem */
font-size: rem(16px); /* 1rem */
/* em() - convert to em */
padding: em(24px); /* 1.5em */
/* light-dark() - color scheme values */
background: light-dark(white, black);
/* alpha() - add opacity to color */
background: alpha(var(--mantine-color-blue-5), 0.5);
}Styles API
Override internal component styles:
classNames Prop
import { TextInput } from "@mantine/core";
import classes from "./TextInput.module.css";
// CSS module with selectors matching Styles API
// .root, .input, .label, .error, etc.
<TextInput
classNames={{
root: classes.root,
input: classes.input,
label: classes.label,
}}
/>;styles Prop (CSS-in-JS)
<TextInput
styles={{
root: { marginBottom: 20 },
input: { backgroundColor: "var(--mantine-color-gray-0)" },
label: { fontWeight: 700 },
}}
/>styles Function
<TextInput
styles={(theme, props) => ({
input: {
borderColor: props.error ? theme.colors.red[6] : theme.colors.gray[4],
},
})}
/>Finding Selectors
All Styles API selectors are documented for each component. Common patterns:
root- Root elementlabel- Label textinput- Input elementwrapper- Input wrappererror- Error messagedescription- Description textrequired- Required asterisksection- Input sections (left/right icons)
hiddenFrom / visibleFrom
Hide/show at breakpoints:
import { Text } from '@mantine/core';
<Text hiddenFrom="sm">Hidden on sm and larger</Text>
<Text visibleFrom="md">Visible only on md and larger</Text>lightHidden / darkHidden
Hide based on color scheme:
<Text lightHidden>Only in dark mode</Text>
<Text darkHidden>Only in light mode</Text>Box Component
Base component for custom styling:
import { Box } from "@mantine/core";
<Box
component="section" // Render as different element
className={classes.wrapper}
p="md"
bg="gray.1"
style={{ borderRadius: "var(--mantine-radius-md)" }}
>
Content
</Box>;Polymorphic Components
Many components accept component prop:
import { Button } from '@mantine/core';
import { Link } from 'react-router-dom';
// Render Button as Link
<Button component={Link} to="/about">
About
</Button>
// Render as native anchor
<Button component="a" href="https://example.com">
External
</Button>CSS Variables in Styles
Access theme values:
<Box
style={{
backgroundColor: "var(--mantine-color-blue-6)",
padding: "var(--mantine-spacing-md)",
borderRadius: "var(--mantine-radius-sm)",
boxShadow: "var(--mantine-shadow-md)",
}}
>
Styled with CSS variables
</Box>Global Styles
// In your CSS
:root {
--my-custom-color: #ff6b6b;
}
/* Target Mantine root element */
[data-mantine-color-scheme="dark"] {
--my-custom-color: #ff8787;
}
/* Global component overrides */
.mantine-Button-root {
font-weight: 600;
}rem() and em() Utilities
import { rem, em } from "@mantine/core";
// In styles or inline
<Box style={{ fontSize: rem(16), padding: rem(24) }} />;
// rem(16) => '1rem'
// em(24) => '1.5em'Style Props vs CSS Modules
| Use Case | Recommended |
|---|---|
| Quick prototyping | Style props |
| Simple spacing/colors | Style props |
| Complex hover/focus states | CSS modules |
| Responsive layouts | CSS modules |
| Reusable component styles | CSS modules |
| Performance critical | CSS modules |
Component Default Props (Theme)
Override defaults globally:
const theme = createTheme({
components: {
Button: Button.extend({
defaultProps: { variant: "outline", size: "md", radius: "xl" },
}),
TextInput: TextInput.extend({
defaultProps: { size: "md" },
classNames: { root: "my-input-root", input: "my-input" },
}),
},
});CSS Variables Reference
--mantine-color-{color}-{shade} // Colors (0-9)
--mantine-primary-color-{shade} // Primary color
--mantine-spacing-{size} // xs, sm, md, lg, xl
--mantine-radius-{size} // xs, sm, md, lg, xl
--mantine-font-family // Main font
--mantine-font-size-{size} // xs, sm, md, lg, xl
--mantine-breakpoint-{size} // Responsive breakpointsTesting Reference
Guide for testing Mantine applications with Vitest and React Testing Library.
Installation
npm install -D vitest jsdom @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-eventVitest Configuration
Add to vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.mjs',
},
});Setup File
Create vitest.setup.mjs:
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';
// Fix for getComputedStyle
const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = () => {};
// Mock matchMedia (required by Mantine)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock ResizeObserver (required by some Mantine components)
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;Custom Render Function
All Mantine components require MantineProvider. Create custom render:
// test-utils/render.tsx
import { render as testingLibraryRender } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { theme } from '../src/theme'; // Your theme if any
export function render(ui: React.ReactNode) {
return testingLibraryRender(<>{ui}</>, {
wrapper: ({ children }) => (
<MantineProvider theme={theme} env="test">
{children}
</MantineProvider>
),
});
}Important: env="test"
Setting env="test" on MantineProvider:
- Disables CSS transitions (tests run faster)
- Disables portals (elements render in place, easier to query)
Export Test Utilities
// test-utils/index.ts
import userEvent from '@testing-library/user-event';
export * from '@testing-library/react';
export { render } from './render';
export { userEvent };Writing Tests
Basic Component Test
// Button.test.tsx
import { render, screen } from '../test-utils';
import { Button } from '@mantine/core';
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});Testing Form Inputs
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { TextInput } from '@mantine/core';
describe('TextInput', () => {
it('accepts user input', async () => {
const onChange = vi.fn();
render(<TextInput onChange={onChange} label="Name" />);
const input = screen.getByLabelText('Name');
await userEvent.type(input, 'John Doe');
expect(input).toHaveValue('John Doe');
expect(onChange).toHaveBeenCalled();
});
it('displays error', () => {
render(<TextInput label="Email" error="Invalid email" />);
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
});Testing useForm
import { render, screen, waitFor } from '../test-utils';
import { userEvent } from '../test-utils';
import { useForm } from '@mantine/form';
import { TextInput, Button } from '@mantine/core';
function TestForm() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { email: '' },
validate: {
email: (v) => (!v.includes('@') ? 'Invalid email' : null),
},
});
return (
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput
label="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Button type="submit">Submit</Button>
</form>
);
}
describe('Form', () => {
it('validates on submit', async () => {
render(<TestForm />);
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
it('clears error on valid input', async () => {
render(<TestForm />);
await userEvent.type(screen.getByLabelText('Email'), 'test@email.com');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.queryByText('Invalid email')).not.toBeInTheDocument();
});
});Testing Modals
With env="test", modals render in place (no portal):
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';
function ModalDemo() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>Open</Button>
<Modal opened={opened} onClose={close} title="Test Modal">
Modal Content
</Modal>
</>
);
}
describe('Modal', () => {
it('opens when button is clicked', async () => {
render(<ModalDemo />);
expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /open/i }));
expect(screen.getByText('Modal Content')).toBeInTheDocument();
});
});Testing Select/Dropdown
import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { Select } from '@mantine/core';
describe('Select', () => {
it('selects an option', async () => {
const onChange = vi.fn();
render(
<Select
label="Country"
data={['USA', 'Canada', 'UK']}
onChange={onChange}
/>
);
// Open dropdown
await userEvent.click(screen.getByLabelText('Country'));
// Select option
await userEvent.click(screen.getByRole('option', { name: 'Canada' }));
expect(onChange).toHaveBeenCalledWith('Canada');
});
});Testing Color Scheme
import { render, screen } from '../test-utils';
import { useMantineColorScheme } from '@mantine/core';
function ColorSchemeToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return (
<button onClick={toggleColorScheme}>
Current: {colorScheme}
</button>
);
}
describe('Color Scheme', () => {
it('toggles color scheme', async () => {
render(<ColorSchemeToggle />);
expect(screen.getByText(/current: light/i)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText(/current: dark/i)).toBeInTheDocument();
});
});Testing Notifications
import { render, screen, waitFor } from '../test-utils';
import { notifications } from '@mantine/notifications';
import { Notifications } from '@mantine/notifications';
import { Button } from '@mantine/core';
function NotificationDemo() {
return (
<>
<Notifications />
<Button onClick={() => notifications.show({ message: 'Hello!' })}>
Show Notification
</Button>
</>
);
}
describe('Notifications', () => {
it('shows notification', async () => {
render(<NotificationDemo />);
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
});
});Testing Hooks
import { renderHook, act } from '@testing-library/react';
import { useDisclosure } from '@mantine/hooks';
describe('useDisclosure', () => {
it('toggles state', () => {
const { result } = renderHook(() => useDisclosure(false));
expect(result.current[0]).toBe(false);
act(() => {
result.current[1].open();
});
expect(result.current[0]).toBe(true);
act(() => {
result.current[1].close();
});
expect(result.current[0]).toBe(false);
});
});Scripts
Add to package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}Common Issues
matchMedia Error
Ensure matchMedia mock is in setup file.
ResizeObserver Error
Ensure ResizeObserver mock is in setup file.
Portal Elements Not Found
Use env="test" on MantineProvider to disable portals.
Transitions Cause Timing Issues
Use env="test" to disable transitions.
Elements Not in Document
Make sure to use custom render that includes MantineProvider.
Testing Checklist
- [ ] Setup file mocks matchMedia and ResizeObserver
- [ ] Custom render includes MantineProvider with
env="test" - [ ] Use
userEventfor user interactions (notfireEvent) - [ ] Use
waitForfor async operations - [ ] Use
getByRolewith accessible names when possible - [ ] Test error states and edge cases