web-ui-mantine-aaf469 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-ui-mantine-aaf469 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Mantine is a batteries-included React component library whose v7 rewrite replaced Emotion with CSS Modules, so there is no CSS-in-JS runtime. Wrap the app inMantineProviderwith acreateThemeobject, installpostcss-preset-mantinefor the mixins andlight-dark(), and style through one of four routes: style props for one-offs, the Styles API (classNames/styles) for inner elements, CSS Modules for custom components, andtheme.componentsfor app-wide defaults.@mantine/form,@mantine/hooks,@mantine/notificationsand@mantine/datesare separate packages, each with its own stylesheet.
Detailed Resources:
virtualColor, component defaults, auto-contrast, theme merginguseForm validation, cross-field rules, nested fields, dynamic lists, schema resolvers<critical_requirements>
Wrap the app in `MantineProvider` and import `@mantine/core/styles.css` at the root. Every component reads its CSS variables from the provider, and without the stylesheet they render as unstyled markup rather than failing loudly.
Install and configure `postcss-preset-mantine`. It compiles @mixin smaller-than, @mixin hover, light-dark() and rem(). Without it those lines pass through to the browser as invalid CSS and are dropped.
Write custom component styles as CSS Modules. createStyles was removed in v7, and the theme's values are available to plain CSS as --mantine-* custom properties.
Give a custom colour all ten shades. The palette is indexed 0 (lightest) to 9 (darkest) and the type is a 10-tuple, so a short array is a compile error rather than a runtime surprise.
</critical_requirements>
Auto-detection: Mantine, @mantine/core, @mantine/hooks, @mantine/form, @mantine/notifications, @mantine/dates, MantineProvider, createTheme, virtualColor, useMantineColorScheme, useComputedColorScheme, ColorSchemeScript, mantineHtmlProps, useDisclosure, useDebouncedValue, postcss-preset-mantine, light-dark(), getInputProps, form.key(), classNames prop, styles prop, Component.extend()
Applies to:
forking them
@mantine/form — validation, nested paths, dynamic listsauto resolution trap@mantine/hooksHandled elsewhere:
--mantine-* custom properties, and how the rest of your stylesheet is organised is settled elsewhere
useDisclosure is a local boolean, not a storeuseForm accepts a resolver, and the schema library that produces it issettled by whatever owns validation
<philosophy>
Mantine is pre-built rather than copy-and-own: you configure components through a theme, not by holding their source. Two consequences shape everything below.
The theme is the customisation surface. Colours, radii, fonts, breakpoints and per-component default props all live in one createTheme object, and every component reads them as CSS variables. An override written at the call site is a decision that will need repeating.
Every component exposes its internals by name. The Styles API gives each inner element a selector — input, label, error, dropdown — reachable through classNames and styles without wrapping or forking the component. That is why v7 could drop CSS-in-JS: nothing needs to run at render time to target an inner node.
</philosophy>
<decision_framework>
One-off spacing or colour → style props: p="md" bg="blue.1"
Every instance of a component → theme.components with Component.extend()
An inner element, with pseudo-classes → classNames + CSS Modules
An inner element, quick and inline → styles prop (no pseudo-classes)
A component of your own → CSS Modules using var(--mantine-*)
Responsive, in JSX → style props with object syntax: w={{ base: "100%", sm: 400 }}
Responsive, in CSS → @mixin smaller-than / larger-than
Light/dark conditional → light-dark() or @mixin light / @mixin darkFull component-selection tree and the Styles API selector table: reference.md.
</decision_framework>
<patterns>
import "@mantine/core/styles.css";
import { createTheme, MantineProvider } from "@mantine/core";
const theme = createTheme({ primaryColor: "blue", defaultRadius: "md" });
export function App() {
return <MantineProvider theme={theme}>{/* app */}</MantineProvider>;
}Create the theme at module scope — inline in the JSX it is a new object every render, and every consumer re-renders with it. Server rendering additionally needs ColorSchemeScript in <head> and mantineHtmlProps on <html>, or the first paint flashes the wrong scheme.
Full code, including postcss.config.cjs: examples/core.md
<Button variant="light" color="red" size="md" radius="sm" loading={isSaving} leftSection={<Icon />}>
Delete
</Button>
<TextInput label="Email" placeholder="[email protected]" withAsterisk error={errors.email} />
<Select label="Role" data={["Admin", "Editor", "Viewer"]} searchable clearable />
<Group gap="md">{/* row */}</Group>
<Stack gap="sm">{/* column */}</Stack>variant, size, color and radius mean the same thing on every component, and Group / Stack replace hand-written flexbox. loading, withAsterisk and error are states the component already renders — reimplementing them costs the built-in accessibility wiring.
const [opened, { open, close }] = useDisclosure(false);
<Modal opened={opened} onClose={close} title="Confirm action" centered>
<Button onClick={close}>Cancel</Button>
</Modal>;Overlays are controlled by opened / onClose, and useDisclosure is the handler set they expect. Compound components — Menu.Target / Menu.Dropdown, Tabs.Tab / Tabs.Panel, Accordion.Item — pair their parts by value, so reordering the list cannot desync them.
Full code: examples/components.md
// Style props: theme values, not CSS values
<Box p="md" bg="blue.1" c="blue.9" fz="sm" w={{ base: "100%", sm: 400 }} />
// Styles API: named inner elements
<TextInput classNames={{ input: classes.input, label: classes.label }} />
<TextInput styles={{ input: { backgroundColor: "var(--mantine-color-blue-0)" } }} />/* your-component.module.css */
.card {
padding: var(--mantine-spacing-md);
background: light-dark(
var(--mantine-color-white),
var(--mantine-color-dark-6)
);
@mixin hover {
box-shadow: var(--mantine-shadow-md);
}
@mixin smaller-than $mantine-breakpoint-sm {
padding: var(--mantine-spacing-sm);
}
}classNames takes classes and so supports pseudo-classes; styles takes inline objects and does not. Both target the same named selectors, listed per component in reference.md.
Full code: examples/core.md
const theme = createTheme({
colors: {
brand: [...BRAND_COLORS], // exactly 10, index 0 lightest
surface: virtualColor({ name: "surface", dark: "dark", light: "gray" }),
},
primaryColor: "brand",
primaryShade: { light: 6, dark: 8 },
components: {
Button: Button.extend({ defaultProps: { variant: "filled", size: "sm" } }),
},
});primaryShade picks a different index per scheme, so one colour name reads correctly in both. virtualColor goes further and swaps the whole palette. Component.extend() sets defaults for every instance and is type-checked against that component's props.
Full code: examples/theming.md
const { setColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme("light");
<ActionIcon
onClick={() => setColorScheme(computed === "dark" ? "light" : "dark")}
>
{computed === "dark" ? <SunIcon /> : <MoonIcon />}
</ActionIcon>;Read the scheme through useComputedColorScheme, not through useMantineColorScheme().colorScheme. The second returns the literal setting, which is "auto" for the default — so a toggle branching on it treats a dark system as light. lightHidden and darkHidden cover the render-one-or-the-other case without a hook at all.
Full code: examples/theming.md
const form = useForm({
mode: "uncontrolled",
initialValues: { email: "", termsAccepted: false },
validate: { email: (v) => (/^\S+@\S+$/.test(v) ? null : "Invalid email") },
});
<TextInput label="Email" withAsterisk key={form.key("email")} {...form.getInputProps("email")} />
<Checkbox label="I accept" key={form.key("termsAccepted")}
{...form.getInputProps("termsAccepted", { type: "checkbox" })} />getInputProps binds value, change handler and error in one spread; { type: "checkbox" } switches it to checked. mode: "uncontrolled" is the default and keeps keystrokes out of React's render path — which is why form.key() is required alongside it, to give React a key that survives the value changing outside its knowledge. Nested paths use dot notation (address.city, members.0.name), and list items move through insertListItem / removeListItem.
Full code, including schema resolvers: examples/forms.md
const [opened, { open, close, toggle }] = useDisclosure(false);
const [debounced] = useDebouncedValue(search, 300);
const isMobile = useMediaQuery("(max-width: 48em)");
const ref = useClickOutside(() => close());
const clipboard = useClipboard({ timeout: 2000 }); // clipboard.copied stays true for the timeoutuseDebouncedValue and useClickOutside own their own cleanup. useMediaQuery returns undefined during server rendering rather than guessing, so branch on that third state instead of treating it as false.
<Notifications position="top-right" />; // once, inside MantineProvider
const id = notifications.show({
loading: true,
message: "Uploading…",
autoClose: false,
});
notifications.update({
id,
loading: false,
message: "Uploaded",
color: "green",
autoClose: 3000,
});The renderer mounts once; the API is imperative and callable from anywhere, including outside React. Update by the returned id rather than showing a second notification.
Full code: examples/components.md
import "@mantine/dates/styles.css";
<DatesProvider settings={{ locale: "en", firstDayOfWeek: 0 }}>
<DatePickerInput
type="range"
label="Date range"
value={range}
onChange={setRange}
/>
</DatesProvider>;Separate package, separate stylesheet, dayjs as a peer. type (default / range / multiple) changes the value's shape rather than the component.
Full code: examples/components.md
</patterns>
<red_flags>
Breaks at runtime:
@mantine/core/styles.css import — components render as unstyled markup with no errorpostcss-preset-mantine — @mixin, light-dark() and rem() reach the browser as invalid CSSand are dropped, so responsive and dark-mode rules silently do nothing
@mantine/dates/styles.css,@mantine/notifications/styles.css and the rest are separate imports
createStyles — removed in v710-tuple
createTheme(...) inline in the JSX — a new theme object every render, re-rendering every consumerSurprising behaviour:
useMantineColorScheme().colorScheme returns "auto" when that is the setting, so a togglebranching on it misreads a dark system as light — useComputedColorScheme resolves it
form.key() is not optional in uncontrolled mode; without it inputs keep stale values after a resetp="md" is theme.spacing.md, and color="blue.6"is shade 6 of the palette rather than a CSS colour
theme.breakpoints — change one and the CSSand the JS disagree with nothing to catch it
useMediaQuery returns undefined during server rendering, which is a third state rather thanfalse
light-dark() here is compiled by the PostCSS preset rather than the native CSS functionstyle prop reaches only the outermost element and takes raw CSS values, so a colour writtenthere is frozen against the colour scheme — inner elements need classNames or styles, and a theme-aware colour needs bg / c or a --mantine-color-* variable
@mantine/dates moved from Date objects to "YYYY-MM-DD" strings,DatesProvider dropped timezone, and CodeHighlight dropped highlight.js. This skill documents v7; check the migration guide before upgrading.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.