8.x → 9.x migration guide

use-form TransformValues type

The second generic type of the useForm hook is now the type of transformed values instead of the transform function type. New usage example:

import { useForm } from '@mantine/form';

interface FormValues {
  name: string;
  locationId: string;
}

interface TransformedValues {
  name: string;
  locationId: number;
}

function Demo() {
  const form = useForm<FormValues, TransformedValues>({
    mode: 'uncontrolled',
    initialValues: {
      name: '',
      locationId: '2',
    },

    transformValues: (values) => ({
      ...values,
      locationId: Number(values.locationId),
    }),
  });
}

Text color prop

The color prop of the Text and Anchor components was removed. Use the c style prop instead:

import { Text } from '@mantine/core';

// ❌ No longer works
function Demo() {
  return <Text color="red">Text</Text>;
}

// ✅ Use the c style prop
function Demo() {
  return <Text c="red">Text</Text>;
}

Form resolvers

The @mantine/form package no longer exports schema resolvers; use dedicated packages instead:

Example with 8.x:

import { z } from 'zod';
// ❌ No longer works; zodResolver is not exported from @mantine/form
import { useForm, zodResolver } from '@mantine/form';

const schema = z.object({
  email: z.string().email({ message: 'Invalid email' }),
});

const form = useForm({
  initialValues: {  email: '' },
  validate: zodResolver(schema),
});

Example with 9.x:

import { z } from 'zod';
import { useForm } from '@mantine/form';
// ✅ Use a dedicated package for the zod resolver
import { zodResolver } from 'mantine-form-zod-resolver';

const schema = z.object({
  email: z.string().email({ message: 'Invalid email' }),
});

const form = useForm({
  initialValues: {  email: '' },
  validate: zodResolver(schema),
});

Async form validation

form.validate(), form.validateField() and form.isValid() now return promises. If your code uses return values from these methods, you need to await them:

import { useForm } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { name: '' },
  validate: {
    name: (value) => (value.trim().length < 2 ? 'Too short' : null),
  },
});

// ❌ No longer works – validate() returns a Promise
const result = form.validate();
if (result.hasErrors) { /* ... */ }

// ✅ Await the result
const result = await form.validate();
if (result.hasErrors) { /* ... */ }

// ❌ No longer works – isValid() returns a Promise
if (form.isValid()) { /* ... */ }

// ✅ Await isValid()
if (await form.isValid()) { /* ... */ }

Validation rules now receive an AbortSignal as the fourth argument. Rules can be async – return a Promise<ReactNode | null> instead of ReactNode | null:

import { useForm } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { username: '' },
  validate: {
    // ✅ Async rule with AbortSignal
    username: async (value, _values, _path, signal) => {
      const res = await fetch(`/api/check?u=${value}`, { signal });
      const data = await res.json();
      return data.taken ? 'Username is taken' : null;
    },
  },
});

New properties and options:

  • form.validatingtrue while any async validation is running
  • form.isValidating(path)true while a specific field is being validated
  • validateDebounce option – debounces field-level validation triggered by validateInputOnChange / validateInputOnBlur
  • resolveValidationError option – custom function to resolve validation errors from rejected promises

Note: form.onSubmit does not require changes – it already handles the async validation internally and calls the success/error callbacks after validation completes.

TypographyStylesProvider

// ❌ No longer works
import { TypographyStylesProvider } from '@mantine/core';

// ✅ Use the Typography component
import { Typography } from '@mantine/core';

Popover and Tooltip positionDependencies prop

The Popover and Tooltip components no longer accept the positionDependencies prop; it is no longer required – the position is now calculated automatically.

import { Popover } from '@mantine/core';

// ❌ positionDependencies is no longer needed
function Demo(props) {
  return (
    <Popover position="top" positionDependencies={[props.a, props.b]}>
      {/* ... */}
    </Popover>
  );
}

// ✅ The position is recalculated automatically
function Demo(props) {
  return (
    <Popover position="top">
      {/* ... */}
    </Popover>
  );
}

use-fullscreen hook changes

The use-fullscreen hook was split into two hooks: useFullscreenElement and useFullscreenDocument. This change was required to fix a stale ref issue in the previous implementation.

New usage with the document element:

import { useFullscreenDocument } from '@mantine/hooks';
import { Button } from '@mantine/core';

function Demo() {
  const { toggle, fullscreen } = useFullscreenDocument();

  return (
    <Button onClick={toggle} color={fullscreen ? 'red' : 'blue'}>
      {fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
    </Button>
  );
}

New usage with a custom target element:

For demo
import { useFullscreenElement } from '@mantine/hooks';
import { Button, Stack } from '@mantine/core';

function RefDemo() {
  const { ref, toggle, fullscreen } = useFullscreenElement();

  return (
    <Stack align="center">
      <img
        ref={ref}
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png"
        alt="For demo"
        width={200}
      />
      <Button onClick={toggle} color={fullscreen ? 'red' : 'blue'}>
        {fullscreen ? 'Exit Fullscreen' : 'View Image Fullscreen'}
      </Button>
    </Stack>
  );
}

use-mouse hook changes

The use-mouse hook was split into two hooks: useMouse and useMousePosition. This change was required to fix a stale ref issue in the previous implementation.

Previous usage with the document element:

import { Text, Code } from '@mantine/core';
import { useMouse } from '@mantine/hooks';

function Demo() {
  const { x, y } = useMouse();

  return (
    <Text ta="center">
      Mouse coordinates <Code>{`{ x: ${x}, y: ${y} }`}</Code>
    </Text>
  );
}

New usage with document:

Mouse coordinates { x: 0, y: 0 }

import { Text, Code } from '@mantine/core';
import { useMousePosition } from '@mantine/hooks';

function Demo() {
  const { x, y } = useMousePosition();

  return (
    <Text ta="center">
      Mouse coordinates <Code>{`{ x: ${x}, y: ${y} }`}</Code>
    </Text>
  );
}

use-mutation-observer hook changes

The use-mutation-observer hook now uses the new callback ref approach. This change was required to fix stale ref issues and improve compatibility with dynamic node changes.

Previous usage (8.x):

import { useMutationObserver } from '@mantine/hooks';

useMutationObserver(
  (mutations) => console.log(mutations),
  { childList: true },
  // ❌ The third argument is no longer supported; use `useMutationObserverTarget` instead
  document.getElementById('external-element')
);

New usage (9.x):

import { useMutationObserverTarget } from '@mantine/hooks';

// ✅ Rename the hook to `useMutationObserverTarget`
useMutationObserverTarget(
  (mutations) => console.log(mutations),
  { childList: true },
  // ✅ Pass the target element as the third argument
  document.getElementById('external-element')
);

Rename hooks types

@mantine/hooks types were renamed for consistency; rename them in your codebase:

  • UseScrollSpyReturnTypeUseScrollSpyReturnValue
  • StateHistoryUseStateHistoryValue
  • OSUseOSReturnValue

Collapse in -> expanded

The Collapse component now uses the expanded prop instead of in:

import { Collapse } from '@mantine/core';

// ❌ No longer works
function Demo() {
  return (
    <Collapse in={false}>
      {/* ... */}
    </Collapse>
  );
}

// ✅ Use the expanded prop
function Demo() {
  return (
    <Collapse expanded={false}>
      {/* ... */}
    </Collapse>
  );
}

Spoiler initialState -> defaultExpanded

The Spoiler component's initialState prop was renamed to defaultExpanded for consistency with other Mantine components:

import { Spoiler } from '@mantine/core';

// ❌ No longer works
function Demo() {
  return (
    <Spoiler initialState={false} showLabel="Show" hideLabel="Hide">
      {/* ... */}
    </Spoiler>
  );
}

// ✅ Use the defaultExpanded prop
function Demo() {
  return (
    <Spoiler defaultExpanded={false} showLabel="Show" hideLabel="Hide">
      {/* ... */}
    </Spoiler>
  );
}

Grid gutter -> gap

The Grid component gutter prop was renamed to gap for consistency with other layout components (like Flex and SimpleGrid). Additionally, new rowGap and columnGap props were added to allow separate control of vertical and horizontal spacing:

import { Grid } from '@mantine/core';

// ❌ No longer works
function Demo() {
  return (
    <Grid gutter="xl">
      <Grid.Col span={6}>1</Grid.Col>
      <Grid.Col span={6}>2</Grid.Col>
    </Grid>
  );
}

// ✅ Use the gap prop
function Demo() {
  return (
    <Grid gap="xl">
      <Grid.Col span={6}>1</Grid.Col>
      <Grid.Col span={6}>2</Grid.Col>
    </Grid>
  );
}

// ✅ New: Separate row and column gaps
function Demo() {
  return (
    <Grid rowGap="xl" columnGap="sm">
      <Grid.Col span={6}>1</Grid.Col>
      <Grid.Col span={6}>2</Grid.Col>
    </Grid>
  );
}

Grid overflow="hidden" no longer required

The Grid component no longer uses negative margins for spacing between columns. It now uses native CSS gap property, so you can safely remove overflow="hidden" from your Grid components — it is no longer needed to prevent content overflow:

import { Grid } from '@mantine/core';

// ❌ overflow="hidden" is no longer needed
function Demo() {
  return (
    <Grid overflow="hidden">
      <Grid.Col span={6}>1</Grid.Col>
      <Grid.Col span={6}>2</Grid.Col>
    </Grid>
  );
}

// ✅ Remove overflow="hidden"
function Demo() {
  return (
    <Grid>
      <Grid.Col span={6}>1</Grid.Col>
      <Grid.Col span={6}>2</Grid.Col>
    </Grid>
  );
}