Skip to content
Back to skills

Mantine

ASecurity

Build React applications with the Mantine UI component library. Use when a user asks to use Mantine components, configure themes, implement forms with validation, add notifications, or build responsive layouts with Mantine hooks.

  • 142 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 29, 2026
developmenttypescriptgoshellbashreactnextjsgit

Works with

  • cursor
  • terminal
  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill mantine --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mantine?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mantine
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-mantine/badge)](https://www.skillsdirectory.com/skills/terminalskills-mantine)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mantine
description: >-
  Build React applications with the Mantine UI component library. Use when a user asks to use Mantine components, configure themes, implement forms with validation, add notifications, or build responsive layouts with Mantine hooks.
license: Apache-2.0
compatibility: "React 19.2+ (Mantine 9); a bundler with PostCSS such as Vite or Next.js"
metadata:
  author: terminal-skills
  version: "1.1.0"
  category: development
  tags: ["react", "ui-library", "components", "hooks", "typescript"]
  repository: https://github.com/mantinedev/mantine
---
# Mantine — Full-Featured React Component Library

## Overview

Mantine is a React component library with 120+ customizable components and 70+ hooks, plus packages for forms, notifications, dates, charts and rich text editing, all written in TypeScript. Since version 7 it ships plain CSS files instead of CSS-in-JS: styles are imported once and customized through the theme, CSS variables and CSS modules. This skill targets Mantine 9 (released March 2026), which requires React 19.2 or later; Example 2 and the Guidelines cover what changed since 7 and 8.

## Instructions

### Setup

```bash
npm install @mantine/core @mantine/hooks @mantine/form @mantine/notifications
npm install @mantine/dates dayjs          # Date components
npm install @mantine/tiptap @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-link  # Rich text editor
npm install --save-dev postcss postcss-preset-mantine postcss-simple-vars
```

The PostCSS preset adds `rem()`, `light-dark()`, `@mixin hover` and the breakpoint variables to your own CSS files. Create `postcss.config.cjs` in the project root:

```js
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",
      },
    },
  },
};
```

### Theme and Provider

```tsx
// src/App.tsx — styles, theme and providers, set up once at the root
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css"; // package styles go after core styles

import { Button, MantineProvider, TextInput, createTheme } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import { Dashboard } from "./Dashboard";

const theme = createTheme({
  primaryColor: "brand",
  fontFamily: "Inter, system-ui, sans-serif",
  colors: {
    // a custom color needs at least 10 shades, lightest first
    brand: ["#f0f0ff", "#d6d6ff", "#b3b3ff", "#8080ff", "#4d4dff", "#1a1aff", "#0000e6", "#0000b3", "#000080", "#00004d"],
  },
  components: {
    Button: Button.extend({ defaultProps: { variant: "light" } }),
    TextInput: TextInput.extend({ defaultProps: { size: "md" } }),
  },
});

export default function App() {
  return (
    <MantineProvider theme={theme} defaultColorScheme="auto">
      <Notifications position="top-right" />
      <Dashboard />
    </MantineProvider>
  );
}
```

### Components

```tsx
// src/Dashboard.tsx — responsive shell: header, collapsible navbar, stat cards
import { AppShell, Avatar, Badge, Burger, Card, Group, Menu, NavLink, SimpleGrid, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { GearIcon, SignOutIcon } from "@phosphor-icons/react";

const stats = [
  { title: "Revenue", value: "$45,200", change: "+12%" },
  { title: "Users", value: "1,234", change: "+5%" },
  { title: "Orders", value: "892", change: "+18%" },
  { title: "Churn", value: "2.1%", change: "-0.3%" },
];

export function Dashboard() {
  const [opened, { toggle }] = useDisclosure();

  return (
    <AppShell header={{ height: 60 }} navbar={{ width: 260, breakpoint: "sm", collapsed: { mobile: !opened } }} padding="md">
      <AppShell.Header>
        <Group h="100%" px="md" justify="space-between">
          <Group>
            <Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" aria-label="Toggle navigation" />
            <Text size="xl" fw={700}>Dashboard</Text>
          </Group>
          <Menu>
            <Menu.Target>
              <Avatar name="Dana Whitfield" color="initials" radius="xl" style={{ cursor: "pointer" }} />
            </Menu.Target>
            <Menu.Dropdown>
              <Menu.Item leftSection={<GearIcon size={14} />}>Settings</Menu.Item>
              <Menu.Divider />
              <Menu.Item color="red" leftSection={<SignOutIcon size={14} />}>Log out</Menu.Item>
            </Menu.Dropdown>
          </Menu>
        </Group>
      </AppShell.Header>

      <AppShell.Navbar p="md">
        <NavLink label="Overview" active />
        <NavLink label="Orders" />
        <NavLink label="Customers" />
      </AppShell.Navbar>

      <AppShell.Main>
        <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
          {stats.map((stat) => (
            <Card key={stat.title} withBorder>
              <Text size="sm" c="dimmed">{stat.title}</Text>
              <Group justify="space-between" mt="xs">
                <Text size="xl" fw={700}>{stat.value}</Text>
                <Badge color={stat.title === "Churn" || stat.change.startsWith("+") ? "teal" : "red"}>{stat.change}</Badge>
              </Group>
            </Card>
          ))}
        </SimpleGrid>
      </AppShell.Main>
    </AppShell>
  );
}
```

### Form Handling

```tsx
// src/SignupForm.tsx — uncontrolled form with validation
import { Button, Checkbox, PasswordInput, Select, Stack, TextInput } from "@mantine/core";
import { hasLength, isEmail, isNotEmpty, useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";

export function SignupForm() {
  const form = useForm({
    mode: "uncontrolled",
    initialValues: { name: "", email: "", password: "", role: "", terms: false },
    validate: {
      name: hasLength({ min: 2 }, "Name too short"),
      email: isEmail("Invalid email"),
      password: hasLength({ min: 8 }, "At least 8 characters"),
      role: isNotEmpty("Select a role"),
      terms: (value) => (value ? null : "You must accept the terms"),
    },
  });

  const handleSubmit = form.onSubmit((values) => {
    notifications.show({ title: "Account created", message: `Welcome, ${values.name}`, color: "teal" });
  });

  return (
    <form onSubmit={handleSubmit}>
      <Stack gap="sm">
        <TextInput label="Name" key={form.key("name")} {...form.getInputProps("name")} />
        <TextInput label="Email" placeholder="dana@northwind.io" key={form.key("email")} {...form.getInputProps("email")} />
        <PasswordInput label="Password" key={form.key("password")} {...form.getInputProps("password")} />
        <Select label="Role" data={["Developer", "Designer", "Product Manager"]} key={form.key("role")} {...form.getInputProps("role")} />
        <Checkbox label="I accept the terms" key={form.key("terms")} {...form.getInputProps("terms", { type: "checkbox" })} />
        <Button type="submit">Create account</Button>
      </Stack>
    </form>
  );
}
```

### Hooks

```tsx
// src/SearchBox.tsx — a few of the 70+ hooks in @mantine/hooks
import { useRef, useState } from "react";
import { Button, Group, TextInput } from "@mantine/core";
import { getHotkeyHandler, useClipboard, useDebouncedValue, useDidUpdate, useHotkeys, useLocalStorage, useMediaQuery } from "@mantine/hooks";

export function SearchBox({ onSearch }: { onSearch: (query: string) => void }) {
  const input = useRef<HTMLInputElement>(null);
  const [search, setSearch] = useState("");
  const [debounced] = useDebouncedValue(search, 300); // updates 300 ms after typing stops
  const clipboard = useClipboard({ timeout: 2000 }); // clipboard.copied stays true for 2 s
  const isMobile = useMediaQuery("(max-width: 48em)"); // false on the server and until the first effect runs

  // without defaultValue the value is typed `string[] | undefined`
  const [recent, setRecent] = useLocalStorage<string[]>({ key: "recent-searches", defaultValue: [] });

  // document-level shortcut; ignored while focus is in an input, textarea or select
  useHotkeys([["mod+K", () => input.current?.focus()]]);

  useDidUpdate(() => onSearch(debounced), [debounced]); // search as you type; skips the first render

  const submit = () => {
    onSearch(search); // not `debounced`: for 300 ms after the last key it still holds the old text
    setRecent([search, ...recent].slice(0, 5));
  };

  return (
    <Group>
      <TextInput
        ref={input}
        placeholder={recent[0] ?? "Search orders"}
        size={isMobile ? "md" : "sm"}
        value={search}
        onChange={(event) => setSearch(event.currentTarget.value)}
        onKeyDown={getHotkeyHandler([["Enter", submit]])} // shortcuts inside an input
      />
      <Button onClick={() => clipboard.copy(search)}>{clipboard.copied ? "Copied" : "Copy query"}</Button>
    </Group>
  );
}
```

## Examples

### Example 1: Add Mantine and a dark-mode toggle to a Vite app

User: "Add Mantine to my Vite + React + TypeScript app and give me a dark mode switch in the header."

```bash
npm install @mantine/core @mantine/hooks @phosphor-icons/react
npm install --save-dev postcss postcss-preset-mantine postcss-simple-vars
```

Add `postcss.config.cjs` from Setup, import `@mantine/core/styles.css` in the root file, wrap the app in `MantineProvider` with `defaultColorScheme="auto"`, then:

```tsx
// src/ColorSchemeToggle.tsx
import { ActionIcon, useComputedColorScheme, useMantineColorScheme } from "@mantine/core";
import { MoonIcon, SunIcon } from "@phosphor-icons/react";

export function ColorSchemeToggle() {
  const { setColorScheme } = useMantineColorScheme();
  // "auto" can mean either; the computed value is always "light" or "dark"
  const computed = useComputedColorScheme("light", { getInitialValueInEffect: true });

  return (
    <ActionIcon variant="default" size="lg" aria-label="Toggle color scheme" onClick={() => setColorScheme(computed === "light" ? "dark" : "light")}>
      {computed === "light" ? <MoonIcon size={18} /> : <SunIcon size={18} />}
    </ActionIcon>
  );
}
```

Result: the app follows the operating system's scheme until the button is clicked; the choice is then kept in `localStorage` and `<html>` carries `data-mantine-color-scheme="dark"` or `"light"`.

### Example 2: Upgrade a Mantine 7 project to 9

User: "We're still on Mantine 7 and React 18. Move us to the current version."

```bash
npm install react@^19.2.0 react-dom@^19.2.0 @types/react@^19 @types/react-dom@^19
npm install @mantine/core@9 @mantine/hooks@9 @mantine/form@9 @mantine/dates@9 @mantine/notifications@9
npx tsc --noEmit   # Vite template (tsconfig with project references): npx tsc -b
```

Update `@tiptap/*` to 3.x and `recharts` to 3.x if `@mantine/tiptap` or `@mantine/charts` are installed. The compiler lists most of what broke, for example:

```
error TS2305: Module '"@mantine/core"' has no exported member 'TypographyStylesProvider'.
error TS2322: Type '{ in: boolean; children: Element; }' is not assignable to type '… CollapseProps …'.
  Property 'in' does not exist on type '… CollapseProps …'.
error TS2322: Type 'Dispatch<SetStateAction<[Date | null, Date | null]>>' is not assignable to type '(value: DatesRangeValue<string>) => void'.
```

Fixes: `TypographyStylesProvider` → `Typography`; `<Collapse in>` → `expanded`; `<Grid gutter>` → `gap`; `<Spoiler initialState>` → `defaultExpanded`; `useFullscreen` → `useFullscreenDocument` or `useFullscreenElement`; `useHeadroom()` returns `{ pinned, scrollProgress }`; `zodResolver(schema)` → `schemaResolver(schema, { sync: true })` from `@mantine/form`; `@mantine/dates` values are strings such as `"2026-10-01"` since 8.0, so `useState<Date | null>` becomes `useState<string | null>`. `<Text color="red">` still compiles but no longer sets the color — search for it and use `c="red"`.

## Guidelines

1. **Use the form library** — `@mantine/form` handles validation, touched state, and nested fields; don't build your own. Prefer `mode: "uncontrolled"` with `key={form.key(...)}` on every input and read values with `form.getValues()`; in this mode `form.values` does not update while typing
2. **Theme over inline styles** — Configure component defaults in the theme with `Component.extend`; avoid prop-based styling on every instance
3. **Hooks for logic** — Mantine's hooks library (`useDisclosure`, `useDebouncedValue`, `useLocalStorage`) reduces boilerplate
4. **AppShell for layouts** — Use `AppShell` for dashboard layouts with navbar, header, aside; `Navbar` and `Header` exist only as `AppShell.Navbar` and `AppShell.Header`, each enabled by its config prop
5. **Notifications system** — Render `<Notifications />` exactly once inside `MantineProvider` and call `notifications.show()` from anywhere; supports queue, auto-close, and custom components
6. **CSS modules, not CSS-in-JS** — Import the `styles.css` of every package you use (`@mantine/core/styles.css` first, then for example `@mantine/dates/styles.css`); missing imports are the usual cause of unstyled date pickers and notifications. `createStyles` and the `sx` prop left `@mantine/core` in 7.0 and live on only in the optional `@mantine/emotion` package
7. **Dark mode built-in** — Use `defaultColorScheme="auto"` for system preference detection. With server rendering add `<ColorSchemeScript />` to `<head>` and spread `mantineHtmlProps` on `<html>` to avoid a flash and a hydration warning
8. **Icons** — Any icon library works. The docs and demos use `@phosphor-icons/react`; `@tabler/icons-react` remains a common choice
9. **Version 9 look changes** — The default radius is now `md` and the `light` variant uses solid colors; set `defaultRadius: "sm"` or pass `cssVariablesResolver={v8CssVariablesResolver}` to keep the 8.x look while migrating
10. **When not to use** — Mantine components are client components: they render on the server but cannot be React server components, and a file that uses hooks or compound components such as `Tabs.Tab` needs `'use client'`. Astro is not supported, and a project that cannot move to React 19.2 has to stay on Mantine 8

Files in this skill

  • SKILL.md7.5 KB
  • _scores.json1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…