apps/mantine.dev/src/pages/guides/8x-to-9x.mdx
import { PopoverDemos, UseFullscreenDemos, UseMouseDemos } from '@docs/demos'; import { Layout } from '@/layout'; import { MDX_DATA } from '@/mdx'; import packageJson from '../../../../../package.json';
export default Layout(MDX_DATA.EightToNine);
You can use LLM agents to assist with the migration from 8.x to 9.x. The LLM documentation includes all breaking changes and migration steps in a format optimized for AI coding tools. Copy the link to the md documentation file and ask your AI agent to perform the migration for you.
<LlmButton href="/llms/guides-8x-to-9x.md" />Mantine 9.x requires React 19.2 or later. If your project uses an older React version, you need to update it before migrating to Mantine 9.x. If you cannot update React to 19.2+ yet, you can continue using Mantine 8.x until you are ready to update React and migrate to Mantine 9.x.
@mantine/* packages to version {packageJson.version}@mantine/tiptap package, update all @tiptap/* packages to the latest 3.x version@mantine/charts package, update recharts to the latest 3.x versionThe 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),
}),
});
}
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>;
}
In Mantine 9, the light variant CSS variables were changed to use solid color values
instead of transparency. If you need to keep 8.x behavior during migration,
use v8CssVariablesResolver:
import {
Button,
MantineProvider,
v8CssVariablesResolver,
} from '@mantine/core';
function Demo() {
return (
<MantineProvider cssVariablesResolver={v8CssVariablesResolver}>
<Button variant="light" color="blue.6">
Uses 8.x light variant colors
</Button>
</MantineProvider>
);
}
In 9.x, @mantine/form has built-in support for Standard Schema.
If your schema library supports Standard Schema (Zod v4, Valibot, ArkType), use the built-in
schemaResolver instead of a dedicated resolver package:
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 using Standard Schema (recommended):
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const schema = z.object({
email: z.email({ error: 'Invalid email' }),
});
const form = useForm({
initialValues: { email: '' },
validate: schemaResolver(schema, { sync: true }),
});
// ❌ No longer works
import { TypographyStylesProvider } from '@mantine/core';
// ✅ Use the Typography component
import { Typography } from '@mantine/core';
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>
);
}
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:
New usage with a custom target element:
<Demo data={UseFullscreenDemos.ref} />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:
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')
);
@mantine/hooks types were renamed for consistency; rename them in your codebase:
UseScrollSpyReturnType → UseScrollSpyReturnValueStateHistory → UseStateHistoryValueOS → UseOSReturnValueThe 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>
);
}
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>
);
}
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>
);
}
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>
);
}
The useLocalStorage and useSessionStorage hooks now correctly include undefined in the
return type when no defaultValue is provided. Previously, calling these hooks without
defaultValue would type the value as T (e.g., string), even though at runtime
the value could be undefined.
If you relied on the incorrect type, update your code to handle undefined:
import { useLocalStorage } from '@mantine/hooks';
// ❌ In 8.x, `value` was typed as `string` (incorrect)
const [value, setValue] = useLocalStorage({ key: 'my-key' });
// ✅ In 9.x, `value` is typed as `string | undefined`
const [value, setValue] = useLocalStorage({ key: 'my-key' });
// ✅ Provide defaultValue to keep the previous non-undefined type
const [value, setValue] = useLocalStorage({
key: 'my-key',
defaultValue: '',
});
The same change applies to readLocalStorageValue, useSessionStorage, and readSessionStorageValue.
In 8.x, the default border-radius (theme.defaultRadius) was sm (4px).
In 9.x, the default border-radius was changed to md (8px).
To keep the previous behavior, set defaultRadius to sm in the theme:
import { createTheme, MantineProvider } from '@mantine/core';
const theme = createTheme({
defaultRadius: 'sm',
});
function Demo() {
return <MantineProvider theme={theme}></MantineProvider>;
}
In 8.x, hovering over a notification paused the auto close timer only for that notification.
In 9.x, the default behavior changed – hovering over any notification now pauses the auto close timer of all
visible notifications. To keep the previous behavior, set pauseResetOnHover="notification":
import { Notifications } from '@mantine/notifications';
function Demo() {
return <Notifications pauseResetOnHover="notification" />;
}