9.7.0
mantinedev/mantine9.7.0Oct 5, 2026by github-actions[bot]
Upgrading?
AI Summary
v9.7.0 introduces four new components—Tour, Toolbar, Toggle, and JsonViewer—alongside major enhancements to RichTextEditor image handling and Mantine's MCP server documentation coverage.
Key Highlights
- New Tour component for interactive application onboarding with spotlight effects.
- RichTextEditor now supports image upload via toolbar, drag-and-drop, or paste with loading states.
- Checkbox and Radio components gain a new `variant="light"` for lighter visual weight.
- Schedule views now support `eventOverlapMode` to handle overlapping events with the new `cascade` layout.
- MCP server documentation coverage expanded to 406 pages for improved search efficiency.
New Features
- New Tour component
- New Toolbar component
- New Toggle component
- New JsonViewer component
- RichTextEditor image upload support
- Light variant for Checkbox and Radio
- Schedule event overlap modes
Full Release Notes
[View changelog with demos on mantine.dev website](https://mantine.dev/changelog/9-7-0)
## Support Mantine development
You can now sponsor Mantine development with [OpenCollective](https://opencollective.com/mantinedev).
All funds are used to improve Mantine and create new features and components.
## @mantine/mcp-server documentation coverage
[@mantine/mcp-server](https://www.npmjs.com/package/@mantine/mcp-server) now exposes 406 documentation
pages instead of 164. Previously only components were indexed – hooks, [@mantine/form](https://mantine.dev/form/use-form),
styling, theming, guides and [help.mantine.dev](https://help.mantine.dev) FAQ pages are now included as
well. Changelog pages are deliberately excluded to keep search results focused on current APIs.
Documentation is now retrieved by section rather than by whole page. `get_item_doc` returns a page
outline (description, import statement and the list of section headings) by default, a single section
with the `section` argument, or the entire page with `full: true`. Requesting `useForm` this way returns
about 1KB instead of 9KB, and the largest pages drop from roughly 89KB to 4KB.
Other improvements:
- **New `get_api` tool** – resolves a symbol such as `useForm` or `useDisclosure` to its TypeScript
signature and exported types without knowing which page documents it
- **Ranked search** – `search_docs` results point at the specific section that matched, so a query like
"validate a form with zod" resolves to the `zod` section of the schema validation page
- **Search synonyms** – queries now match the search keywords maintained in the documentation, so
"command palette" finds [Spotlight](https://mantine.dev/x/spotlight)
- **More filters** – `list_items` accepts `group`, `category` and `package` in addition to `kind`
- **Hook signatures** – `get_item_props` returns the TypeScript signature for hooks, and points at the
relevant section for pages that document their API in prose
The server was also migrated to the official Model Context Protocol SDK, so the protocol version is
negotiated with the client instead of being pinned.
For setup details, supported tools, and client-specific instructions, see [Mantine with LLMs](https://mantine.dev/guides/llms).
## Tour component
New [Tour](https://mantine.dev/core/tour) component provides interactive step-by-step application onboarding.
It highlights target elements with a spotlight overlay and displays tooltips with navigation controls.
Supports both guided (sequential) and beacon (non-sequential) modes.
## Toolbar component
New [Toolbar](https://mantine.dev/core/toolbar) component provides an accessible toolbar container with roving tabindex
keyboard navigation, toggle groups with single/multiple selection, visual grouping, and dividers.
Styling is similar to [RichTextEditor](https://mantine.dev/x/tiptap) toolbar.
## Toggle component
New [Toggle](https://mantine.dev/core/toggle) component is a button that can be toggled between active and inactive states:
## JsonViewer component
New [JsonViewer](https://mantine.dev/code-highlight/json-viewer) component can be used to display complex JSON
structures with expand/collapse, type indicators, copy to clipboard, and other features:
## Light Checkbox and Radio variant
[Checkbox](https://mantine.dev/core/checkbox) and [Radio](https://mantine.dev/core/radio) components now support `variant="light"` –
the indicator uses a light background color with a colored icon, similar to the light variant
available in other components like [Button](https://mantine.dev/core/button):
```tsx
import { Radio, Checkbox, Stack } from '@mantine/core';
function Demo() {
return (
<Stack gap={7}>
<Checkbox variant="light" label="Light Checkbox" defaultChecked />
<Checkbox variant="light" label="Light indeterminate Checkbox" indeterminate />
<Radio variant="light" label="Light Radio" defaultChecked />
</Stack>
);
}
```
## RichTextEditor image upload
[RichTextEditor](https://mantine.dev/x/tiptap) now supports image upload with loading states.
Images can be uploaded via toolbar button, drag and drop, or paste.
During upload, a local preview is shown with a loading indicator.
Configuring `getUploadImageExtension` is enough to enable uploads – `onImageUpload` and
`onImageUploadError` props of `RichTextEditor` are optional overrides for the toolbar control.
`onImageUpload` can resolve either to a string or to `{ src, alt, title }` (new exported
`ImageUploadResult` type) to set alternative text on the uploaded image. Several files picked,
dropped or pasted together are inserted in the order they were selected and are undone as one step.
```tsx
import TipTapImage from '@tiptap/extension-image';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { getUploadImageExtension, ImageUploadResult, RichTextEditor } from '@mantine/tiptap';
function handleImageUpload(file: File): Promise<ImageUploadResult> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ src: URL.createObjectURL(file), alt: file.name });
}, 3000);
});
}
function Demo() {
const editor = useEditor({
shouldRerenderOnTransaction: true,
extensions: [
StarterKit,
getUploadImageExtension(TipTapImage, { onImageUpload: handleImageUpload }),
],
content: '<p>Click the image button in the toolbar, or drag & drop / paste an image into the editor.</p>',
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.ImageUpload />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}
```
## Schedule event overlap mode
[DayView](https://mantine.dev/schedule/day-view) and [WeekView](https://mantine.dev/schedule/week-view) now support `eventOverlapMode` prop
that controls how events overlapping in time share the horizontal space. The default `columns` mode
splits the available width evenly between them, which becomes unreadable once a large number of events
overlap. The new `cascade` mode indents each event and stacks it over the previous one, so every event
keeps a readable width no matter how many of them overlap.
The indent shrinks automatically as the group grows, so the event on top always keeps at least 60% of
the available width. Covered events can still be read and used: rest the pointer on one and it is
raised above the events covering it, which also makes its resize handles reachable. Keyboard users get
the same result by focusing the event with `Tab`. `eventOverlapRaiseDelay` prop controls how long the
pointer must rest on an event before it is raised, in milliseconds (`600` by default).
```tsx
import { useState } from 'react';
import { SegmentedControl, Stack } from '@mantine/core';
import { ScheduleEventOverlapMode, WeekView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
const [mode, setMode] = useState<ScheduleEventOverlapMode>('cascade');
return (
<Stack>
<SegmentedControl
value={mode}
onChange={(value) => setMode(value as ScheduleEventOverlapMode)}
data={[
{ value: 'columns', label: 'columns' },
{ value: 'cascade', label: 'cascade' },
]}
/>
<WeekView
date={new Date()}
events={events}
startTime="08:00:00"
endTime="14:00:00"
eventOverlapMode={mode}
/>
</Stack>
);
}
```
## Schedule drop validation
All [Schedule](https://mantine.dev/schedule/schedule) views can now reject a drag or resize **before** it is committed,
based on where the event is being dropped rather than on which event is being dragged. Previously
`canDragEvent` and `canResizeEvent` only decided whether an event was draggable or resizable at all,
so the only way to prevent a double booking was to accept the drop, notice the conflict in the
handler and not update state – which produced a snap-back with no explanation:
- `preventEventOverlap` rejects placements that would make the event overlap another event. Pass a
function to decide per pair of events: return `true` to forbid that particular overlap.
- `canDropEvent` and `canResizeEventTo` are called before a drop or resize is committed – return
`false` to reject it.
- `canDropExternalEvent` does the same for items dragged in from outside the schedule.
- `onEventPlacementRejected` reports the rejection with the conflicting events and the reason.
The checks compose: a placement is allowed only when the overlap check passes and the matching
callback returns a value other than `false`. The overlap check runs first, so `onEventPlacementRejected`
can report `conflicts` without your code repeating the work. While the pointer is over an invalid
target the drag ghost and the resizing event get `data-invalid`, so the rejection is visible before the
pointer is released.
```tsx
import { useState } from 'react';
import dayjs from 'dayjs';
import { Stack, Text } from '@mantine/core';
import { WeekView, ScheduleEventData } from '@mantine/schedule';
const startOfWeek = dayjs()
.subtract((dayjs().day() + 6) % 7, 'day')
.format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Standup',
start: `${startOfWeek} 09:00:00`,
end: `${startOfWeek} 09:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Design review',
start: `${startOfWeek} 11:00:00`,
end: `${startOfWeek} 12:00:00`,
color: 'grape',
},
{
id: 3,
title: 'Retro',
start: `${startOfWeek} 14:00:00`,
end: `${startOfWeek} 15:00:00`,
color: 'teal',
},
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState(initialEvents);
const [message, setMessage] = useState<string | null>(null);
return (
<Stack>
<WeekView
date={date}
onDateChange={setDate}
events={events}
startTime="08:00:00"
endTime="18:00:00"
withEventsDragAndDrop
withEventResize
eventDragInterval={15}
preventEventOverlap
onEventDrop={({ eventId, newStart, newEnd }) => {
setMessage(null);
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
}}
onEventResize={({ eventId, newStart, newEnd }) => {
setMessage(null);
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
}}
onEventPlacementRejected={(data) => {
if (data.action === 'external-drop') {
return;
}
setMessage(
`${data.event.title} cannot be ${data.action === 'drop' ? 'moved' : 'resized'} there – it would overlap ${data.conflicts
.map((conflict) => conflict.title)
.join(', ')}`
);
}}
/>
{message && <Text c="red" size="sm">{message}</Text>}
</Stack>
);
}
```
In [resource views](https://mantine.dev/schedule/resources-schedule) the conflict set is scoped to the **target**
resource, so moving an event onto another resource row is checked against that row rather than the one
it came from:
```tsx
import { useState } from 'react';
import dayjs from 'dayjs';
import { Stack, Text } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const resources: ScheduleResourceData[] = [
{ id: 'room-a', label: 'Room A' },
{ id: 'room-b', label: 'Room B' },
{ id: 'room-c', label: 'Room C (large groups only)' },
];
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Interview (2 people)',
start: `${today} 09:00:00`,
end: `${today} 10:00:00`,
color: 'blue',
resourceId: 'room-a',
payload: { attendees: 2 },
},
{
id: 2,
title: 'All-hands (40 people)',
start: `${today} 11:00:00`,
end: `${today} 12:00:00`,
color: 'grape',
resourceId: 'room-c',
payload: { attendees: 40 },
},
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState(initialEvents);
const [message, setMessage] = useState<string | null>(null);
return (
<Stack>
<ResourcesDayView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
startTime="08:00:00"
endTime="18:00:00"
withEventsDragAndDrop
preventEventOverlap
canDropEvent={({ event, resourceId }) =>
resourceId !== 'room-c' || (event.payload?.attendees ?? 0) >= 20
}
onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
setMessage(null);
setEvents((prev) =>
prev.map((event) =>
event.id === eventId
? { ...event, start: newStart, end: newEnd, resourceId }
: event
)
);
}}
onEventPlacementRejected={(data) => {
if (data.action === 'external-drop') {
return;
}
setMessage(
data.reason === 'overlap'
? `${data.event.title} would overlap ${data.conflicts.length} event(s) in that room`
: 'Room C is reserved for meetings with 20 or more attendees'
);
}}
/>
{message && <Text c="red" size="sm">{message}</Text>}
</Stack>
);
}
```
## TreeSelect renderPill
[TreeSelect](https://mantine.dev/core/tree-select) now supports `renderPill` prop to customize how pills are rendered
for selected values in `multiple` and `checkbox` modes, similar to `renderPill` prop of
[MultiSelect](https://mantine.dev/core/multi-select). The callback receives the tree node, so pills can use
`nodeProps` or the node structure, for example to show a different icon for parent and leaf nodes:
```tsx
import { FileTextIcon, FolderSimpleIcon } from '@phosphor-icons/react';
import { Group, Pill, TreeSelect, TreeSelectProps } from '@mantine/core';
import { data } from './data';
const renderTreePill: TreeSelectProps['renderPill'] = ({ node, onRemove, disabled, readOnly }) => (
<Pill withRemoveButton={!readOnly} onRemove={onRemove} disabled={disabled}>
<Group gap={4} wrap="nowrap">
{node.children || node.hasChildren ? (
<FolderSimpleIcon size={12} />
) : (
<FileTextIcon size={12} />
)}
{node.label}
</Group>
</Pill>
);
function Demo() {
return (
<TreeSelect
mode="checkbox"
checkedStrategy="parent"
label="Categories"
placeholder="Pick categories"
data={data}
defaultValue={['phones', 'headphones']}
defaultExpandAll
renderPill={renderTreePill}
/>
);
}
```
## Tooltip hideDetached
[Tooltip](https://mantine.dev/core/tooltip) now supports `hideDetached` prop, which works the same way as
[Popover hideDetached](https://mantine.dev/core/popover/#hide-detached). It is enabled by default: the tooltip is now
hidden when its target element is removed from the DOM, hidden with `display: none`, or scrolled out
of a container that clips it. Previously, the tooltip stayed in place, floating over unrelated
content. Set `hideDetached={false}` to restore the previous behavior. The prop has no effect on
`Tooltip.Floating`, which is positioned relative to the mouse pointer.
Note that jsdom reports zero-size rectangles for every element, so in Jest and Vitest every tooltip
is treated as detached and hidden unless the test wrapper sets `env="test"` on `MantineProvider`
(as described in the [Jest](https://mantine.dev/guides/jest) and [Vitest](https://mantine.dev/guides/vitest) guides) or the component is
rendered with `hideDetached={false}`.
```tsx
import { Box, Button, Group, Tooltip } from '@mantine/core';
function Demo() {
return (
<Box
bd="1px solid var(--mantine-color-dimmed)"
p="xl"
w={{ base: 340, sm: 400 }}
h={200}
style={{ overflow: 'auto' }}
>
<Box w={1000} h={400}>
<Group>
<Tooltip label="Hidden when detached" position="bottom" opened>
<Button>Hides when detached</Button>
</Tooltip>
<Tooltip label="Visible when detached" position="bottom" opened hideDetached={false}>
<Button>Stays visible</Button>
</Tooltip>
</Group>
</Box>
</Box>
);
}
```
## Notifications auto close progress
[Notifications](https://mantine.dev/x/notifications) now support `withAutoCloseProgress` prop. When set, a progress line
that fills up until the notification auto closes is displayed at the bottom of every notification.
The line fades out while the auto close timer is paused (for example, when the notification is hovered),
starts over when the timer restarts, and uses the notification color. The prop sets the default for all notifications, it can be overridden
for individual notifications with `withAutoCloseProgress` property in `notifications.show` and
`notifications.update` functions:
```tsx
import { Button, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';
function Demo() {
return (
<Group justify="center">
<Button
onClick={() =>
notifications.show({
title: 'Auto close progress',
message: 'This notification will close in 5 seconds',
withAutoCloseProgress: true,
autoClose: 5000,
})
}
>
Show notification with progress
</Button>
<Button
color="red"
onClick={() =>
notifications.show({
title: 'Something went wrong',
message: 'Progress line uses notification color',
color: 'red',
withAutoCloseProgress: true,
autoClose: 8000,
})
}
>
Red notification with progress
</Button>
</Group>
);
}
```
## Notifications promise
New `notifications.promise` function displays a loading notification while a promise is pending and
updates it with success or error state once the promise settles. `success` and `error` can be
functions that receive the resolved value or the rejection reason:
```tsx
import { Button, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';
function saveDocument(shouldFail: boolean) {
return new Promise<{ name: string }>((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error('Network error'));
} else {
resolve({ name: 'report.pdf' });
}
}, 2000);
});
}
function Demo() {
const save = (shouldFail: boolean) =>
notifications.promise(saveDocument(shouldFail), {
loading: {
title: 'Saving document',
message: 'Please wait, it will take a couple of seconds',
},
success: (value) => ({
title: 'Document saved',
message: `${value.name} was saved successfully`,
}),
error: (error) => ({
title: 'Failed to save document',
message: (error as Error).message,
}),
});
return (
<Group justify="center">
<Button onClick={() => save(false)}>Save document</Button>
<Button color="red" onClick={() => save(true)}>
Save document (fails)
</Button>
</Group>
);
}
```
## AppShell resizable sections
[AppShell](https://mantine.dev/core/app-shell) now supports resizing `AppShell.Navbar`/`AppShell.Aside`
horizontally and `AppShell.Header`/`AppShell.Footer` vertically. Pass a `useAppShellResize`
hook instance to the `resize` prop to enable it for the configured sections – handles support
mouse, touch and keyboard, and can report drag-to-collapse. Each section accepts a `label` option
to localize the `aria-label` of its handle.
See [AppShell examples](https://mantine.dev/app-shell/?e=ResizableAppShell) for more details.
Note that when the `resize` prop is used, `--app-shell-*` CSS variables are defined on the
`AppShell` root element instead of `:root`, so they cannot be read from content rendered in a
[Portal](https://mantine.dev/core/portal), for example from a [Modal](https://mantine.dev/core/modal) or a [Drawer](https://mantine.dev/core/drawer).
## Templates and guides
- New [mantine-tanstack-start-template](https://github.com/mantinedev/mantine-tanstack-start-template) and [TanStack Start guide](https://mantine.dev/guides/tanstack-start) to set up Mantine in a TanStack Start application.
- [vite-template](https://github.com/mantinedev/vite-template) now uses [Vite+](https://viteplus.dev/): Vite, Vitest, oxlint and oxfmt run through the `vp` CLI and are configured in `vite.config.mjs`.
- New [StyleX guide](https://mantine.dev/styles/stylex) explains how to style Mantine components with [StyleX](https://stylexjs.com/) in a Vite application.
## Other changes
- [HoverCard](https://mantine.dev/core/hover-card) now opens on keyboard focus, closes with `Escape` or a press outside, and is announced by screen readers. New `events`, `interactive`, `role` and `returnFocus` props, and `onDismiss` callback.
- [HoverCard](https://mantine.dev/core/hover-card) no longer opens with a tap on touch devices. Set `events={{ focus: false, touch: true }}` and `closeOnEscape={false}` to restore the previous behavior.
- [Checkbox](https://mantine.dev/core/checkbox) and [Radio](https://mantine.dev/core/radio) `variant="light"` colors are resolved with `theme.variantColorResolver`, so any CSS color or theme color with a shade can be used.
- [ActionBar](https://mantine.dev/core/action-bar) with `keepMounted` now uses React `Activity` instead of `display: none`, so effects are paused while the bar is closed. Set `transitionProps={{ keepMountedMode: 'display-none' }}` to restore the previous behavior.
- [Mantine skills](https://mantine.dev/guides/llms/#skills) for AI coding agents (`mantine-form`, `mantine-combobox`, `mantine-custom-components`) have been updated for 9.x with corrected API details and new patterns.
- Gatsby and Redwood guides have been removed, `gatsby-template` and `redwood-template` are no longer maintained. Existing applications can keep using Mantine, the setup does not change.
Related Repositories
openclaw/openclaw★ 392,706
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
nilbuild/developer-roadmap★ 370,075
Interactive roadmaps, guides and other educational content to help developers grow in their careers.
morluto/rea★ 358,411
Reverse engineer anything with agents, from app behavior down to native binaries.
anomalyco/opencode★ 219,729
The open source coding agent.
n8n-io/n8n★ 208,261
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.