dist/plugins/web-ui-mantine/skills/web-ui-mantine/SKILL.md
Mantine v7 component library — theming, styling, hooks, forms, notifications
npx skillsauth add agents-inc/skills web-ui-mantineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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>
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:
When NOT to use:
Key patterns covered:
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:
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.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",
},
},
},
};
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
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="[email protected]"
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
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
Mantine v7 uses CSS Modules as the primary styling approach. The PostCSS preset provides responsive mixins and light-dark().
/* 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>
);
}
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 },
}}
/>
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
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.
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
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>
);
}
const form = useForm({
mode: "uncontrolled",
initialValues: { password: "", confirmPassword: "" },
validate: {
confirmPassword: (value, values) =>
value !== values.password ? "Passwords do not match" : null,
},
});
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.
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)
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
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
@mantine/datescomponents fromDateobjects to"YYYY-MM-DD"strings.DatesProviderno longer supports thetimezoneoption.
Detailed Resources:
<red_flags>
High Priority Issues:
createStyles - Removed in v7. Use CSS Modules (.module.css) instead@mixin smaller-than), light-dark(), and rem() will not work@mantine/core/styles.css import - Components will render unstyledMedium Priority Issues:
colorScheme directly for toggle logic - Returns "auto" when set to auto; use useComputedColorScheme to get resolved "light" or "dark"form.key() in uncontrolled mode - Required for React key stability when using mode: "uncontrolled" with getInputProps@mantine/dates/styles.css, @mantine/notifications/styles.css must be imported separately if using those packagesstyle prop - Use classNames/styles Styles API or CSS Modules to target inner elements properlyGotchas & Edge Cases:
mantineHtmlProps required for SSR - Must be spread on <html> element to prevent hydration mismatchesColorSchemeScript must be in <head> - Prevents flash of wrong color scheme before hydrationp="md" resolves to theme.spacing.md, not the string "md"color="blue.6" accesses shade 6 of the blue palette, not a CSS color nameuseMediaQuery returns undefined during SSR - Handle the initial render state to prevent hydration mismatchtheme.breakpoints, update postcss.config.cjs variables too@mantine/dates uses "YYYY-MM-DD" strings instead of Date objects, DatesProvider drops timezone, CodeHighlight drops highlight.js. Check migration guide if upgradinglight-dark() is a PostCSS function, not native CSS - Requires postcss-preset-mantine to compile</red_flags>
<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>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety