Skills web-ui-mantine
Mantine v7 component library — theming, styling, hooks, forms, notifications
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/web-ui-mantine/skills/web-ui-mantine" ~/.claude/skills/agents-inc-skills-web-ui-mantine && rm -rf "$T"
dist/plugins/web-ui-mantine/skills/web-ui-mantine/SKILL.mdMantine 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
with aMantineProviderobject. Style with CSS Modules + PostCSS preset mixins, the Styles API (createTheme/classNamesprops), or style props (styles,p,m, etc.). Usebgfor form management,@mantine/formfor utility hooks, and@mantine/hooksfor toasts. PostCSS preset with@mantine/notificationsis required for responsive mixins andpostcss-preset-mantine.light-dark()
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST wrap your app in
and import MantineProvider
at the root)@mantine/core/styles.css
(You MUST install and configure
for CSS Modules mixins and responsive utilities)postcss-preset-mantine
(You MUST use CSS Modules (
) for custom component styles - .module.css
was removed in v7)createStyles
(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>
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
<patterns>
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
</patterns>v8 note: Mantine v8 changed all
components from@mantine/datesobjects toDatestrings."YYYY-MM-DD"no longer supports theDatesProvideroption.timezone
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
- Removed in v7. Use CSS Modules (createStyles
) instead.module.css - Missing PostCSS preset - Responsive mixins (
),@mixin smaller-than
, andlight-dark()
will not workrem() - Missing
import - Components will render unstyled@mantine/core/styles.css - Providing fewer than 10 shades for custom colors - Mantine requires exactly 10-shade tuples, TypeScript will error
Medium Priority Issues:
- Using
directly for toggle logic - ReturnscolorScheme
when set to auto; use"auto"
to get resolveduseComputedColorScheme
or"light""dark" - Forgetting
in uncontrolled mode - Required for React key stability when usingform.key()
withmode: "uncontrolled"getInputProps - Not importing package-specific CSS -
,@mantine/dates/styles.css
must be imported separately if using those packages@mantine/notifications/styles.css - Overriding styles with inline
prop - Usestyle
/classNames
Styles API or CSS Modules to target inner elements properlystyles
Gotchas & Edge Cases:
required for SSR - Must be spread onmantineHtmlProps
element to prevent hydration mismatches<html>
must be inColorSchemeScript
- Prevents flash of wrong color scheme before hydration<head>- Style props use theme values, not CSS values -
resolves top="md"
, not the stringtheme.spacing.md"md" - Color dot notation -
accesses shade 6 of the blue palette, not a CSS color namecolor="blue.6"
returnsuseMediaQuery
during SSR - Handle the initial render state to prevent hydration mismatchundefined- PostCSS breakpoint variables must match Mantine defaults - If you change
, updatetheme.breakpoints
variables toopostcss.config.cjs - v8 exists (released May 2025) - Key v8 changes:
uses@mantine/dates
strings instead of"YYYY-MM-DD"
objects,Date
dropsDatesProvider
,timezone
drops highlight.js. Check migration guide if upgradingCodeHighlight
is a PostCSS function, not native CSS - Requireslight-dark()
to compilepostcss-preset-mantine
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST wrap your app in
and import MantineProvider
at the root)@mantine/core/styles.css
(You MUST install and configure
for CSS Modules mixins and responsive utilities)postcss-preset-mantine
(You MUST use CSS Modules (
) for custom component styles - .module.css
was removed in v7)createStyles
(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>