← All posts
·19 min read

Claude Code with Mantine: React UI Components Guide

Claude CodeMantineReactUI
Claude Code with Mantine: 120+ components for React

Why Mantine without CLAUDE.md generates components that fight the theme system

Mantine is one of the most complete React component libraries available in 2026. Over 120 components, a full hooks library, first-class TypeScript support, and a theming system that covers every token from border radius to font family. The problem is that Claude Code does not know which version of Mantine you are on, which packages you have installed, or how your theme is configured. Without explicit constraints, Claude reaches for patterns from Mantine 5 and 6 that no longer exist in Mantine 7, imports from @mantine/styles which was removed, applies raw CSS overrides instead of the styles prop, hardcodes hex values instead of using theme color arrays, and generates components that bypass the theme entirely by wrapping everything in a Box with inline style props.

The result is a component library that looks disconnected. Some components pull from the theme, others are styled independently. Dark mode works on the Mantine-aware components and breaks on the custom ones. Color changes require updates in twelve places instead of one. None of that is Mantine failing. It is Claude generating components that bet on local overrides rather than the token system the library was built around.

This guide covers the CLAUDE.md configuration that anchors Claude Code to Mantine 7's actual model: a single MantineProvider at the root, a createTheme object that defines your design tokens, a consistent prop API across every component, and the hooks that handle colour scheme, forms, and notifications. If you are setting up Claude Code for the first time, the Claude Code setup guide covers installation. For the broader React context this operates within, Claude Code with React is the right starting point.

The Mantine CLAUDE.md template

The CLAUDE.md at your project root is read at the start of every session. For a Mantine project it needs to declare: Mantine version and package list, the MantineProvider wrapping requirement, the createTheme structure you are using, the prop API that applies across all components, the import paths that are correct in Mantine 7, the dark mode setup with ColorSchemeScript, the form binding pattern, and the hard rules that block the patterns Claude generates most often without guidance.

# Mantine UI rules

## Stack
- Mantine 7.x (@mantine/core, @mantine/hooks, @mantine/form, @mantine/notifications)
- React 18.x, TypeScript 5.x strict
- Next.js 14.x (App Router) OR Vite 5.x. Confirm per project.

## Package imports
- Components: import from '@mantine/core' ONLY
- Hooks: import from '@mantine/hooks' ONLY
- Forms: import from '@mantine/form' ONLY
- Notifications: import from '@mantine/notifications' ONLY
- NEVER import from '@mantine/styles'. This package was removed in Mantine 7.
- NEVER import from '@mantine/next'. Use @mantine/core directly with Next.js App Router.

## App root setup (required)
- Wrap the entire app in <MantineProvider theme={theme}> from '@mantine/core'
- Include <ColorSchemeScript /> in the <head> before any other scripts (prevents dark mode flash)
- Include <Notifications /> inside <MantineProvider> if using @mantine/notifications
- theme object must be created with createTheme() from '@mantine/core'

## Theme structure (createTheme)
import { createTheme } from '@mantine/core';

export const theme = createTheme({
  primaryColor: 'blue',           // must match a key in theme.colors
  defaultRadius: 'md',            // xs | sm | md | lg | xl or px value
  fontFamily: 'Inter, sans-serif',
  fontFamilyMonospace: 'JetBrains Mono, monospace',
  colors: {
    brand: [
      '#e8f4fd', '#c5e4fa', '#9ed0f7', '#71b9f3',
      '#4da6ef', '#339AF0', '#2688d4', '#1a75b8',
      '#0f619b', '#064d7e',
    ],
  },
  components: {
    Button: {
      defaultProps: {
        radius: 'md',
      },
    },
  },
});

## Color array rules
- Every color in theme.colors is a 10-shade tuple indexed 0-9 (lightest to darkest)
- theme.colors.blue[5] is the primary shade used by Mantine by default
- NEVER access theme.colors.blue[10] or beyond. The array is exactly 10 items, index 0-9.
- When adding a custom color (e.g. 'brand'), ALWAYS provide all 10 shades
- Use the color name string in component props, not the hex value:
  <Button color="brand"> not <Button color="#339AF0">

## Component prop API (applies to ALL Mantine components)
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
- variant: 'filled' | 'outline' | 'light' | 'subtle' | 'transparent' | 'white' | 'default'
- color: any key from theme.colors or CSS color string
- radius: 'xs' | 'sm' | 'md' | 'lg' | 'xl' or number (px)
- Example: <Button size="md" variant="filled" color="blue" radius="md">Save</Button>

## Styling priority (use in this order)
1. Component props (size, color, variant, radius): use for standard variations
2. styles prop for granular slot overrides:
   <TextInput styles={{ root: { marginBottom: 8 }, label: { fontWeight: 700 }, input: { borderColor: 'red' } }} />
3. className prop with CSS modules: use for layout or custom positioning
4. style prop: ONLY for truly one-off dynamic values (e.g. width calculated from state)
5. NEVER override Mantine component internals with global CSS selectors (.mantine-Button-root)

## Layout primitives (use these, not Box for everything)
- Stack: vertical layout with gap prop. Example: <Stack gap="md">
- Group: horizontal layout with gap and justify props. Example: <Group justify="space-between">
- Grid and Grid.Col: CSS grid. Example: <Grid><Grid.Col span={6}>
- SimpleGrid: equal-width columns. Example: <SimpleGrid cols={3}>
- Box: generic container, use only when Stack/Group/Grid do not fit
- Flex: explicit flex control when Stack/Group are not enough
- NEVER use Box where Stack or Group would work

## Dark mode (useMantineColorScheme)
import { useMantineColorScheme, ActionIcon } from '@mantine/core';
import { IconSun, IconMoon } from '@tabler/icons-react';

function ColorSchemeToggle() {
  const { colorScheme, setColorScheme } = useMantineColorScheme();
  return (
    <ActionIcon
      onClick={() => setColorScheme(colorScheme === 'dark' ? 'light' : 'dark')}
      variant="default"
      size="xl"
    >
      {colorScheme === 'dark' ? <IconSun /> : <IconMoon />}
    </ActionIcon>
  );
}
- ALWAYS include <ColorSchemeScript /> in head to prevent flash on load
- NEVER set colorScheme manually via CSS variables. Use setColorScheme only.
- 'auto' is a valid value: setColorScheme('auto') follows the OS preference

## Forms (@mantine/form)
import { useForm } from '@mantine/form';

const form = useForm({
  initialValues: {
    email: '',
    password: '',
    rememberMe: false,
  },
  validate: {
    email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
    password: (value) => (value.length < 8 ? 'Password must be at least 8 characters' : null),
  },
});

// Bind to form element (required: not just onSubmit handler)
<form onSubmit={form.onSubmit(handleSubmit)}>
  <TextInput label="Email" {...form.getInputProps('email')} />
  <PasswordInput label="Password" {...form.getInputProps('password')} />
  <Checkbox label="Remember me" {...form.getInputProps('rememberMe', { type: 'checkbox' })} />
  <Button type="submit">Login</Button>
</form>

- ALWAYS use form.onSubmit() wrapper on the <form> element, not a bare onSubmit handler
- ALWAYS spread form.getInputProps('fieldName') onto each input component
- Checkbox and Switch ALWAYS need { type: 'checkbox' } in getInputProps
- form.setValues() for programmatic updates, form.reset() to clear
- form.validate() returns { hasErrors: boolean } for manual pre-submit checks

## Notifications (@mantine/notifications)
import { notifications } from '@mantine/notifications';

notifications.show({
  title: 'Order confirmed',
  message: 'Your order #1234 has been placed.',
  color: 'green',
  autoClose: 5000,
});

notifications.show({
  title: 'Payment failed',
  message: 'Check your card details and try again.',
  color: 'red',
  autoClose: false,   // user must dismiss
});

- ALWAYS wrap app in <Notifications /> inside <MantineProvider>
- Import notifications (lowercase) from '@mantine/notifications', it is a singleton object, not a hook
- autoClose: number (ms) or false (no auto-dismiss)
- id prop on show() lets you update the notification: notifications.update({ id, title, message })

## Hard rules
- NEVER import from '@mantine/styles', package removed in Mantine 7
- NEVER access theme.colors.*.10 or beyond, arrays are exactly 10 items (index 0-9)
- NEVER wrap <form> onSubmit with a raw handler; always use form.onSubmit()
- NEVER use Box where Stack or Group would work
- NEVER override Mantine internals with global CSS (.mantine-Button-root selectors)
- ALWAYS include <ColorSchemeScript /> in head (prevents dark mode flash)
- ALWAYS provide all 10 shades when adding a custom color to theme.colors

Four rules here prevent the majority of broken Mantine integrations Claude generates without them.

The never-import-from-@mantine/styles rule is the most urgent entry for any project upgrading from Mantine 5 or 6. That package no longer exists in Mantine 7. Claude's training data includes Mantine 5 patterns, and import { createStyles } from '@mantine/styles' appears in a significant portion of pre-2024 Mantine tutorials and Stack Overflow answers. The rule forces the correct alternative: the styles prop on individual components, CSS modules, or the createTheme component-level defaultProps and styles keys.

The color array bounds rule matters because Mantine's color arrays are always exactly 10 items, indexed 0 through 9. Claude will sometimes generate theme.colors.blue[10] when trying to access a very dark shade, which returns undefined and produces a transparent or fallback-colored element. The rule eliminates out-of-bounds access and anchors Claude to the correct index range.

The form.onSubmit wrapper rule is subtle. <form onSubmit={handleSubmit}> looks correct but bypasses Mantine's validation trigger. form.onSubmit(handleSubmit) wraps your handler and runs all field validators before calling your function. Without the wrapper, a user can submit a form with failing validation rules and the validate configuration in useForm is never called. Claude generates the unwrapped version by default because it is the standard React pattern. The rule enforces the Mantine-specific wrapper.

The Stack/Group over Box rule improves both the generated code quality and the readability of future changes. Box with inline style props is a CSS escape hatch. Stack and Group express layout intent: vertical sequence with spacing, or horizontal group with alignment. Claude defaults to Box when it does not know which layout primitive fits. Declaring the layout vocabulary in CLAUDE.md makes Claude reach for the right primitive first.

MantineProvider setup and theme override

The MantineProvider is the single most important structural requirement in a Mantine project. Every component inside it reads from the theme object. Every component outside it falls back to Mantine's default theme, which means your primaryColor, fontFamily, and custom colors have no effect on those components.

For a Next.js 14 App Router project, the setup lives in app/layout.tsx:

import { MantineProvider, ColorSchemeScript } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import { theme } from '../theme';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <ColorSchemeScript />
      </head>
      <body>
        <MantineProvider theme={theme}>
          <Notifications />
          {children}
        </MantineProvider>
      </body>
    </html>
  );
}

For a Vite project, the setup lives in src/main.tsx:

import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import { theme } from './theme';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <MantineProvider theme={theme}>
      <Notifications />
      <App />
    </MantineProvider>
  </React.StrictMode>
);

Two points Claude misses without the CLAUDE.md template. First, the CSS imports. @mantine/core/styles.css is required for Mantine 7. Mantine 6 injected styles automatically. Without the explicit import, components render with no visual styling. Second, the ColorSchemeScript placement. It must appear in <head> before any rendering output. If it is inside <body> or inside MantineProvider, the script runs after the initial paint, and users on dark mode OS preference see a white flash before the dark theme loads.

The createTheme function and theme tokens

The createTheme function takes a partial theme object and merges it with Mantine's defaults. You only specify what you are overriding. Every key is optional. Claude will generate a complete theme object when the structure is not specified, which means it picks values for keys you have not decided on yet. The CLAUDE.md template shows the exact keys your project uses, keeping the generated theme focused.

The most consequential keys for visual consistency are primaryColor, defaultRadius, and fontFamily. Once these are in the theme, Claude generates components that inherit them automatically rather than specifying them inline on every component:

import { createTheme, MantineColorsTuple } from '@mantine/core';

const brandColor: MantineColorsTuple = [
  '#e8f4fd', '#c5e4fa', '#9ed0f7', '#71b9f3',
  '#4da6ef', '#339AF0', '#2688d4', '#1a75b8',
  '#0f619b', '#064d7e',
];

export const theme = createTheme({
  primaryColor: 'brand',
  defaultRadius: 'md',
  fontFamily: 'Inter, sans-serif',
  fontFamilyMonospace: 'JetBrains Mono, monospace',
  colors: {
    brand: brandColor,
  },
  headings: {
    fontFamily: 'Inter, sans-serif',
    fontWeight: '700',
  },
  components: {
    Button: {
      defaultProps: {
        radius: 'md',
      },
    },
    TextInput: {
      defaultProps: {
        radius: 'md',
      },
    },
    Select: {
      defaultProps: {
        radius: 'md',
      },
    },
  },
});

The MantineColorsTuple type enforces the 10-item array at compile time. If you provide 9 or 11 shades, TypeScript flags it immediately. This is the correct way to add custom colors, and adding it to the CLAUDE.md template means Claude generates the type annotation alongside the array, not just the raw string array.

The components key in createTheme is where you set project-wide component defaults. Rather than adding radius="md" to every Button across the codebase, you set it once here. Claude will add radius to individual components by default. The theme-level default is the more maintainable pattern, and the CLAUDE.md template shows both where it lives and the correct syntax.

Component style props and the styles prop

Every Mantine component accepts the same core prop set: size, variant, color, radius. This consistency is the central design decision in Mantine 7. Claude generates correct props for the first few components it creates in a session, but without an explicit policy it drifts toward mixing inline style props with component props as the file grows longer.

The styles prop is the correct escalation path when component props are not enough. It accepts an object keyed by the internal slot names of the component:

// TextInput has slots: root, wrapper, label, description, input, error, section
<TextInput
  label="Email address"
  description="We will never share your email"
  styles={{
    root: { marginBottom: 16 },
    label: { fontWeight: 700, color: 'var(--mantine-color-brand-7)' },
    input: { fontFamily: 'JetBrains Mono, monospace' },
    error: { fontSize: 12 },
  }}
/>

Each component's slot names are documented in the Mantine docs under the "Styles API" tab. Claude cannot know these slot names from context alone. The CLAUDE.md template shows the TextInput example with the common slots, which gives Claude the pattern to apply to other components.

The CSS variable syntax (var(--mantine-color-brand-7)) is how theme tokens are accessed from within styles prop values. Mantine generates CSS custom properties from your theme object, one per shade per color. This is preferable to hardcoding hex values because it automatically updates when your theme changes. Claude will use hex values by default. The CLAUDE.md example shows the variable syntax so Claude learns to reach for it.

For the broader question of how Claude Code handles styling decisions across a React project, including the boundary between Mantine's built-in system and utility-first approaches, Claude Code with Tailwind covers the complementary patterns and when combining both libraries is reasonable.

useMantineColorScheme for dark mode

Mantine's dark mode system is fully automatic when configured correctly. The useMantineColorScheme hook returns the current scheme and a setter. Components inside MantineProvider respond to scheme changes without any additional configuration because the scheme change updates CSS custom properties at the root level.

import { useMantineColorScheme, useComputedColorScheme, Group, Button } from '@mantine/core';

function ColorSchemeToggle() {
  const { setColorScheme } = useMantineColorScheme();
  const computedColorScheme = useComputedColorScheme('light');

  return (
    <Group>
      <Button
        variant={computedColorScheme === 'light' ? 'filled' : 'outline'}
        onClick={() => setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')}
      >
        Toggle colour scheme
      </Button>
    </Group>
  );
}

The distinction between useMantineColorScheme and useComputedColorScheme is something Claude conflates without the CLAUDE.md rule. useMantineColorScheme returns { colorScheme, setColorScheme } where colorScheme can be 'light', 'dark', or 'auto'. When the scheme is 'auto', the user's OS preference determines the actual rendered scheme, but colorScheme stays 'auto' rather than resolving to 'light' or 'dark'. If you use colorScheme to conditionally render a sun or moon icon, 'auto' produces neither. useComputedColorScheme('light') resolves 'auto' to the actual OS-derived value, with 'light' as the fallback if the OS preference is unknown. Use useComputedColorScheme for conditional rendering and setColorScheme from useMantineColorScheme for changes.

The ColorSchemeScript in the document <head> is what prevents the flash. It is a blocking script that reads the stored colour scheme preference from localStorage before React hydrates, and sets the CSS custom properties at the HTML root immediately. Without it, the initial paint uses light mode (the HTML default), and users on dark mode preference see a white flash before React sets the correct scheme. Claude will omit ColorSchemeScript when generating the layout if it is not specified in CLAUDE.md.

Form handling with @mantine/form

@mantine/form is a lightweight, validation-first form library built to integrate with Mantine inputs. The integration point is form.getInputProps(fieldName), which returns { value, onChange, onBlur, error } in a shape that every Mantine input component accepts directly via spread. This is the pattern that makes Mantine forms feel cohesive: one line of getInputProps wires up the entire field.

import { useForm } from '@mantine/form';
import { TextInput, PasswordInput, Checkbox, Button, Stack, Paper, Title } from '@mantine/core';

interface LoginFormValues {
  email: string;
  password: string;
  rememberMe: boolean;
}

function LoginForm() {
  const form = useForm<LoginFormValues>({
    initialValues: {
      email: '',
      password: '',
      rememberMe: false,
    },
    validate: {
      email: (value) =>
        /^\S+@\S+\.\S+$/.test(value) ? null : 'Enter a valid email address',
      password: (value) =>
        value.length >= 8 ? null : 'Password must be at least 8 characters',
    },
  });

  const handleSubmit = async (values: LoginFormValues) => {
    // values is typed LoginFormValues, all fields validated before this runs
    await loginUser(values);
  };

  return (
    <Paper withBorder p="xl" radius="md">
      <Title order={2} mb="md">Sign in</Title>
      <form onSubmit={form.onSubmit(handleSubmit)}>
        <Stack gap="md">
          <TextInput
            label="Email address"
            placeholder="you@example.com"
            {...form.getInputProps('email')}
          />
          <PasswordInput
            label="Password"
            placeholder="Your password"
            {...form.getInputProps('password')}
          />
          <Checkbox
            label="Keep me signed in"
            {...form.getInputProps('rememberMe', { type: 'checkbox' })}
          />
          <Button type="submit" fullWidth>
            Sign in
          </Button>
        </Stack>
      </form>
    </Paper>
  );
}

Three patterns Claude gets wrong without the CLAUDE.md template.

The form.onSubmit wrapper. <form onSubmit={handleSubmit}> bypasses Mantine's validation. form.onSubmit(handleSubmit) runs the validate functions first and only calls handleSubmit if all fields pass. The symptom of the unwrapped version is that the form submits with validation errors still displayed, because handleSubmit runs before the errors are set.

The checkbox type annotation. form.getInputProps('rememberMe') returns { value: false, onChange, onBlur } where value is a boolean. But Checkbox expects its checked state via checked, not value. Spreading the default getInputProps output onto a Checkbox wires value to the wrong prop. form.getInputProps('rememberMe', { type: 'checkbox' }) switches the return shape to { checked: false, onChange }, which is the correct shape for Checkbox and Switch.

The generic type on useForm. Claude generates useForm({ initialValues: {...} }) without a type parameter. The form values are then typed as whatever TypeScript infers from the initial values. That works for string fields but produces boolean instead of boolean for checkboxes and string instead of a union type for selects. Passing useForm<LoginFormValues>({ ... }) with an explicit interface makes the validate functions and the handleSubmit argument fully typed.

For the server-side integration patterns when this form submits to a Next.js API route or a FastAPI endpoint, Claude Code with Next.js covers the Server Actions and API route conventions that connect to form submissions.

Notifications with @mantine/notifications

@mantine/notifications is a standalone package that provides a global notification system. The API is a singleton object: notifications.show(), notifications.update(), notifications.hide(). You do not import a hook. You import the object and call it from anywhere, including non-component files like API utility functions.

import { notifications } from '@mantine/notifications';
import { IconCheck, IconX } from '@tabler/icons-react';

// Success notification after an action
async function saveProfile(data: ProfileData) {
  try {
    await api.post('/profile', data);
    notifications.show({
      id: 'save-profile',
      title: 'Profile saved',
      message: 'Your profile has been updated successfully.',
      color: 'green',
      icon: <IconCheck size={16} />,
      autoClose: 4000,
    });
  } catch (error) {
    notifications.show({
      id: 'save-profile-error',
      title: 'Save failed',
      message: 'Could not save your profile. Please try again.',
      color: 'red',
      icon: <IconX size={16} />,
      autoClose: false,
    });
  }
}

// Update an in-progress notification (e.g. file upload)
function startUpload(file: File) {
  notifications.show({
    id: 'upload',
    title: 'Uploading',
    message: `Uploading ${file.name}...`,
    loading: true,
    autoClose: false,
  });

  uploadFile(file).then(() => {
    notifications.update({
      id: 'upload',
      title: 'Upload complete',
      message: `${file.name} was uploaded successfully.`,
      color: 'green',
      loading: false,
      autoClose: 3000,
    });
  });
}

The id prop on notifications.show() enables the update pattern. Without an id, each call to notifications.show() creates a new independent notification. With a matching id, notifications.update({ id, ... }) modifies the existing notification in place. This is the correct pattern for long-running operations like file uploads or form submissions to a slow API.

Claude will generate notifications.show() calls without id props by default. For simple status alerts that do not need updating, that is correct. The CLAUDE.md template shows the id pattern for the cases that need it, so Claude learns to apply it when a loading state is involved.

The loading: true flag renders a Loader spinner inside the notification and removes the close button. Setting loading: false in the update call replaces the spinner with the icon. Claude does not know about the loading prop without the CLAUDE.md example, and will instead show two separate notifications (one for in-progress, one for complete) which is visually noisier.

Mantine hooks: useDisclosure, useDebouncedValue, useLocalStorage

@mantine/hooks provides utility hooks that complement the component library. Claude can use these when they are named in CLAUDE.md. Without the reference, Claude generates manual implementations using useState and useEffect for patterns the library already covers.

import { useDisclosure, useDebouncedValue, useLocalStorage, useMediaQuery } from '@mantine/hooks';
import { Modal, Button, TextInput, Badge } from '@mantine/core';

// useDisclosure: open/close state for modals, drawers, menus
function ConfirmModal() {
  const [opened, { open, close }] = useDisclosure(false);

  return (
    <>
      <Button onClick={open}>Delete account</Button>
      <Modal opened={opened} onClose={close} title="Are you sure?">
        <Button color="red" onClick={close}>Confirm delete</Button>
      </Modal>
    </>
  );
}

// useDebouncedValue: debounce a search input before firing an API call
function SearchInput() {
  const [value, setValue] = useState('');
  const [debounced] = useDebouncedValue(value, 400);

  useEffect(() => {
    if (debounced) searchApi(debounced);
  }, [debounced]);

  return (
    <TextInput
      value={value}
      onChange={(e) => setValue(e.currentTarget.value)}
      placeholder="Search..."
    />
  );
}

// useLocalStorage: persisted state with the same interface as useState
function ThemePreference() {
  const [fontSize, setFontSize] = useLocalStorage({
    key: 'app-font-size',
    defaultValue: 'md',
  });

  return (
    <Badge onClick={() => setFontSize(fontSize === 'md' ? 'lg' : 'md')}>
      Font: {fontSize}
    </Badge>
  );
}

// useMediaQuery: responsive logic in components
function ResponsiveHeader() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  return <Group gap={isMobile ? 'xs' : 'xl'}>{/* ... */}</Group>;
}

The useDisclosure hook is the one most commonly reinvented by Claude. The manual version is const [opened, setOpened] = useState(false) with onClick={() => setOpened(true)} and onClick={() => setOpened(false)} on the trigger and close button. useDisclosure returns [opened, { open, close, toggle }] which is more readable at the call site and handles the common toggle case without needing to read the current value. When Modal, Drawer, and Popover components appear in the same file, CLAUDE.md guidance toward useDisclosure produces cleaner code than Claude's default useState pattern.

Common Mantine gotchas Claude hits without CLAUDE.md

Several Mantine patterns are counterintuitive enough that Claude generates incorrect code consistently, regardless of how much Mantine context it has seen in training.

Missing CSS imports. Mantine 7 requires explicit CSS imports. import '@mantine/core/styles.css' must appear once in the app entry point. If you use @mantine/dates, add import '@mantine/dates/styles.css'. If you use @mantine/notifications, add import '@mantine/notifications/styles.css'. Claude will generate the component usage correctly but omit the CSS imports, producing a correctly structured but visually unstyled component.

Box overuse. Claude's default layout component when it does not have guidance is Box. A column of form fields becomes a Box with sx={{ display: 'flex', flexDirection: 'column', gap: 16 }}. The Mantine equivalent is <Stack gap="md">. The CLAUDE.md rule naming Stack, Group, Grid, SimpleGrid, and Flex as the layout primitives redirects Claude away from this pattern immediately.

Accessing @mantine/styles. Any tutorial or Stack Overflow answer written before late 2023 that uses createStyles from @mantine/styles is showing a Mantine 5 or 6 pattern. Claude reproduces it when no explicit version constraint is present. The hard rule in CLAUDE.md eliminates this before Claude has a chance to generate the import.

Color index out of bounds. theme.colors.blue is an array with 10 items. Index 9 is the darkest. theme.colors.blue[10] is undefined. This produces no TypeScript error if you access it via a dynamic index. Using the MantineColorsTuple type on custom color definitions catches the 10-item requirement at the array declaration, and the CLAUDE.md rule catches runtime access via rule rather than type.

Form without onSubmit wrapper. The symptom is subtle: the form appears to work, validation errors show after submit, but the handleSubmit function also runs with invalid values. The validate functions run but the result is not checked before calling the handler. The fix is always form.onSubmit(handleSubmit).

Notifications without the Notifications component. notifications.show() silently does nothing if <Notifications /> is not rendered inside MantineProvider. Claude generates the notifications.show() call correctly but omits the provider setup in the app root if it is not shown in CLAUDE.md.


The Mantine CLAUDE.md in this guide produces a component library where every component reads from the theme because the MantineProvider wraps the entire app, colours are consistent because all components use the theme token system instead of inline hex values, dark mode is flash-free because ColorSchemeScript is in the document head, forms validate correctly because form.onSubmit() wraps every submission handler, and notifications update in place because every long-running operation uses the id prop pattern.

The underlying principle is the same as any framework integration with Claude Code. A Mantine project without a CLAUDE.md produces components that work in isolation and diverge from the design system as the codebase grows, because Claude reaches for local overrides when theme tokens are not specified. A project with the configuration above has a single source of truth: the createTheme object. Every generated component inherits from it automatically.

For the mechanics of how CLAUDE.md is read at session start and how to version it alongside your component library, see CLAUDE.md explained. Claudify includes a Mantine-specific CLAUDE.md template, pre-configured for the MantineProvider setup, createTheme token discipline, form binding rules, notification provider setup, and hook usage shown in this guide.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame