9.6.0
mantinedev/mantine9.6.0Aug 31, 2026by github-actions[bot]
AI Summary
Mantine 9.6.0 introduces a new dedicated Lightbox package, enhanced Notifications with custom rendering and stacked layouts, and a new ActionBar component for bulk selection actions.
Key Highlights
- New `@mantine/lightbox` package with zoom and video support
- Notifications now support custom rendering and stacked layout
- New `ActionBar` component for bulk actions
- Sponsorship support for development
New Features
- New `@mantine/lightbox` package
- Notifications `renderNotification` prop
- Notifications `layout="stacked"`
- New `ActionBar` component
- Sponsorship support via OpenCollective
Full Release Notes
[View changelog with demos on mantine.dev website](https://mantine.dev/changelog/9-6-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/lightbox package
New [@mantine/lightbox](https://mantine.dev/x/lightbox) package – a full-screen media lightbox with carousel navigation,
zoom, thumbnails, toolbar customization, and store-based API. Supports image, video, and custom slides:
```tsx
import '@mantine/lightbox/styles.css';
import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';
const images = [
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];
const slides: LightboxSlideData[] = images.map((src) => ({ src }));
function Demo() {
const [opened, setOpened] = useState(false);
const [index, setIndex] = useState(0);
return (
<>
<Lightbox
opened={opened}
onClose={() => setOpened(false)}
slides={slides}
currentIndex={index}
onIndexChange={setIndex}
/>
<SimpleGrid cols={3}>
{images.map((src, i) => (
<Image
key={src}
src={src}
radius="md"
style={{ cursor: 'pointer' }}
onClick={() => {
setIndex(i);
setOpened(true);
}}
/>
))}
</SimpleGrid>
</>
);
}
```
Key features:
- **Zoom** – click to zoom on desktop, double-tap on mobile, scroll wheel and pinch gestures
- **Thumbnails** – bottom thumbnail strip with active indicator
- **Store API** – mount once, open from anywhere (same pattern as Spotlight and Notifications)
- **Video slides** – native video player with auto-pause on navigation
- **Custom slides** – render anything with custom thumbnails
- **Transitions** – animated open and close with configurable `transitionProps` (same API as Modal)
- **Keyboard shortcuts** – Escape, arrows, F/T/Z for fullscreen/thumbnails/zoom
- **Localization** – every string is defined in the `labels` prop
## Notifications custom rendering
[Notifications](https://mantine.dev/x/notifications) now support `renderNotification` prop that allows you to completely
replace the default notification with custom content. All animations (enter, exit, drag dismiss)
are preserved for custom notifications:
```tsx
import { Avatar, Button, Group, rem, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';
function Demo() {
return (
<Group justify="center">
<Button
onClick={() =>
notifications.show({
autoClose: false,
renderNotification: (notification) => (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: rem(12),
padding: rem(16),
borderRadius: rem(8),
backgroundColor: 'var(--mantine-color-body)',
border: '1px solid var(--mantine-color-default-border)',
boxShadow: 'var(--mantine-shadow-lg)',
userSelect: 'none',
}}
>
<Avatar src={null} radius="xl" color="blue">
DM
</Avatar>
<div style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={600}>
Dan sent you a message
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
Hey, are you free for a quick call?
</Text>
<Group gap="xs" mt={8}>
<Button
size="compact-xs"
variant="filled"
onClick={() =>
notifications.hide(notification.id!)
}
>
Reply
</Button>
<Button
size="compact-xs"
variant="default"
onClick={() =>
notifications.hide(notification.id!)
}
>
Dismiss
</Button>
</Group>
</div>
</div>
),
message: '',
})
}
>
Show custom notification
</Button>
</Group>
);
}
```
## Notifications stacked layout
[Notifications](https://mantine.dev/x/notifications) now support `layout="stacked"` prop that displays notifications in a stacked
layout where only the latest notification is fully visible, and older notifications peek out behind it:
```tsx
import { Button, Group } from '@mantine/core';
import { Notifications, notifications } from '@mantine/notifications';
function Demo() {
return (
<>
{/* Replace your existing Notifications with layout="stacked" */}
<Notifications layout="stacked" />
<Group justify="center">
<Button
onClick={() => {
notifications.show({
title: 'New notification',
message: 'This notification is part of a stacked layout',
});
}}
>
Show stacked notification
</Button>
</Group>
</>
);
}
```
## ActionBar component
New [ActionBar](https://mantine.dev/core/action-bar) component – a fixed-position bottom bar
for bulk selection actions. Designed to be controlled by table or checkbox
selections, it provides a set of actions that can be performed on selected items.
```tsx
import { useState } from 'react';
import { ActionBar, Button, Checkbox, Table, Text } from '@mantine/core';
const elements = [
{ position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' },
{ position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' },
{ position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' },
{ position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' },
{ position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' },
];
function Demo() {
const [selection, setSelection] = useState<number[]>([]);
const toggleRow = (position: number) =>
setSelection((current) =>
current.includes(position)
? current.filter((item) => item !== position)
: [...current, position]
);
const toggleAll = () =>
setSelection((current) =>
current.length === elements.length ? [] : elements.map((element) => element.position)
);
const rows = elements.map((element) => (
<Table.Tr
key={element.position}
bg={selection.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
<Table.Td>
<Checkbox
aria-label="Select row"
checked={selection.includes(element.position)}
onChange={() => toggleRow(element.position)}
/>
</Table.Td>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Checkbox
aria-label="Select all"
checked={selection.length === elements.length}
indeterminate={selection.length > 0 && selection.length !== elements.length}
onChange={toggleAll}
/>
</Table.Th>
<Table.Th>Element position</Table.Th>
<Table.Th>Element name</Table.Th>
<Table.Th>Symbol</Table.Th>
<Table.Th>Atomic mass</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
<ActionBar opened={selection.length > 0} onClose={() => setSelection([])} shadow="md">
<Text size="sm">{selection.length} selected</Text>
<ActionBar.Divider />
<Button variant="default" size="compact-sm">
Delete
</Button>
<Button variant="default" size="compact-sm">
Move
</Button>
<Button variant="default" size="compact-sm">
Archive
</Button>
<ActionBar.CloseButton />
</ActionBar>
</>
);
}
```
## RichTextEditor table controls
[RichTextEditor](https://mantine.dev/x/tiptap) now includes a set of controls for editing tables. Install and register the
[Tiptap table extension](https://tiptap.dev/docs/editor/extensions/nodes/table) (`TableKit`), then add
the controls to the toolbar. `RichTextEditor.TableInsert` opens a grid to pick the table size, and the
other controls add/remove rows and columns, toggle header row/column and merge/split cells. All table
controls are automatically disabled when the cursor is not inside a table:
```tsx
import { TableKit } from '@tiptap/extension-table';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';
function Demo() {
const editor = useEditor({
extensions: [StarterKit, TableKit],
content: `
<table>
<tbody>
<tr><th><p>Framework</p></th><th><p>Language</p></th></tr>
<tr><td><p>Mantine</p></td><td><p>TypeScript</p></td></tr>
<tr><td><p>Tiptap</p></td><td><p>TypeScript</p></td></tr>
</tbody>
</table>
<p></p>
`,
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup>
<RichTextEditor.TableInsert />
<RichTextEditor.TableDelete />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.TableColumnBefore />
<RichTextEditor.TableColumnAfter />
<RichTextEditor.TableColumnDelete />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.TableRowBefore />
<RichTextEditor.TableRowAfter />
<RichTextEditor.TableRowDelete />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.TableToggleHeaderRow />
<RichTextEditor.TableToggleHeaderColumn />
<RichTextEditor.TableMergeCells />
<RichTextEditor.TableSplitCell />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}
```
## RichTextEditor Details control
[RichTextEditor](https://mantine.dev/x/tiptap) now supports collapsible sections. Install and register the
[Tiptap details extension](https://tiptap.dev/docs/editor/extensions/nodes/details) (`Details`,
`DetailsSummary` and `DetailsContent`), then add `RichTextEditor.Details` to the toolbar. The control
wraps the current block in a collapsible details node, or removes it when the cursor is already inside
one:
```tsx
import { Details, DetailsSummary, DetailsContent } from '@tiptap/extension-details';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';
function Demo() {
const editor = useEditor({
extensions: [StarterKit, Details, DetailsSummary, DetailsContent],
content: `
<details>
<summary>Shipping and delivery</summary>
<p>Orders are processed within 1–2 business days and delivered in 3–5 business days.</p>
</details>
<details>
<summary>Returns and refunds</summary>
<p>You can return any item within 30 days of delivery for a full refund.</p>
</details>
<p></p>
`,
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Details />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}
```
To support the control, [Typography](https://mantine.dev/core/typography) now styles `details` and `summary` elements –
a border, padding and a custom disclosure triangle. This applies to all `details` elements inside
`Typography`, not just those created by the editor. All of the new selectors have zero specificity
(`:where()`), so they can be overridden without `!important`.
## RichTextEditor InvisibleCharacters control
[RichTextEditor](https://mantine.dev/x/tiptap) can now display formatting marks. Install and register the
[Tiptap invisible characters extension](https://tiptap.dev/docs/editor/extensions/functionality/invisiblecharacters),
then add `RichTextEditor.InvisibleCharacters` to the toolbar. The control toggles the visibility of
spaces, paragraph breaks and hard breaks, and reflects the current visibility as its active state:
```tsx
import InvisibleCharacters from '@tiptap/extension-invisible-characters';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';
function Demo() {
const editor = useEditor({
extensions: [StarterKit, InvisibleCharacters.configure({ visible: false })],
content: `
<p>Toggle the control to reveal spaces and paragraph breaks.</p>
<p>Each space becomes a dot and every paragraph ends with a pilcrow.</p>
`,
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.InvisibleCharacters />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}
```
## GaugeChart component
New [GaugeChart](https://mantine.dev/charts/gauge-chart) component – a radial gauge chart for KPI and status display.
Supports threshold sections, target marker, custom labels, and configurable arc angles.
```tsx
import { GaugeChart } from '@mantine/charts';
function Demo() {
return <GaugeChart value={72} size={200} thickness={12} />;
}
```
## WaffleChart component
New [WaffleChart](https://mantine.dev/charts/waffle-chart) component – a part-to-whole grid chart with colored cells.
Simpler and more compact alternative to pie/donut charts for displaying percentages and proportions.
```tsx
// Demo.tsx
import { WaffleChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return <WaffleChart data={data} />;
}
// data.ts
import { WaffleChartCell } from '@mantine/charts';
export const data: WaffleChartCell[] = [
{ name: 'Chrome', value: 65, color: 'blue' },
{ name: 'Safari', value: 19, color: 'teal' },
{ name: 'Firefox', value: 10, color: 'orange' },
{ name: 'Other', value: 6, color: 'gray' },
];
```
## MatrixChart component
New [MatrixChart](https://mantine.dev/charts/matrix-chart) component – a generic x/y heatmap with categorical axes.
Each cell is colored based on a value, useful for visualizing patterns in two-dimensional categorical data.
```tsx
// Demo.tsx
import { MatrixChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<MatrixChart
data={data}
yLabels={['James', 'Mary', 'Robert', 'Linda', 'Michael', 'Sarah', 'David', 'Emma']}
withYLabels
withTooltip
getTooltipLabel={({ x, y, value }) =>
`${y}, Mar ${x}: ${value === null ? 'No contributions' : `${value} contribution${value > 1 ? 's' : ''}`}`
}
/>
);
}
// data.ts
import { MatrixChartCell } from '@mantine/charts';
export const data: MatrixChartCell[] = [
{ x: '1', y: 'James', value: 7 },
{ x: '2', y: 'James', value: 10 },
{ x: '3', y: 'James', value: 2 },
{ x: '4', y: 'James', value: 10 },
{ x: '5', y: 'James', value: 8 },
{ x: '6', y: 'James', value: null },
{ x: '7', y: 'James', value: null },
{ x: '8', y: 'James', value: 6 },
{ x: '9', y: 'James', value: 2 },
{ x: '10', y: 'James', value: 8 },
{ x: '11', y: 'James', value: 1 },
{ x: '12', y: 'James', value: 3 },
{ x: '13', y: 'James', value: 7 },
{ x: '14', y: 'James', value: null },
{ x: '15', y: 'James', value: 9 },
{ x: '16', y: 'James', value: 10 },
{ x: '17', y: 'James', value: null },
{ x: '18', y: 'James', value: 1 },
{ x: '19', y: 'James', value: 8 },
{ x: '20', y: 'James', value: null },
{ x: '21', y: 'James', value: null },
{ x: '22', y: 'James', value: 5 },
{ x: '23', y: 'James', value: 8 },
{ x: '24', y: 'James', value: 2 },
{ x: '25', y: 'James', value: 5 },
{ x: '26', y: 'James', value: 6 },
{ x: '27', y: 'James', value: null },
{ x: '28', y: 'James', value: null },
{ x: '29', y: 'James', value: 7 },
{ x: '30', y: 'James', value: 7 },
{ x: '31', y: 'James', value: 6 },
{ x: '1', y: 'Mary', value: 3 },
{ x: '2', y: 'Mary', value: 1 },
// ... remaining data
];
```
## CandlestickChart component
New [CandlestickChart](https://mantine.dev/charts/candlestick-chart) component – a financial OHLC chart that displays
open, high, low and close values as candles. Candles are colored based on their direction, the wick
shows the high–low range and the body shows the open–close range. Supports custom colors, data keys,
reference lines, axis labels, tooltip labels and value formatting.
```tsx
// Demo.tsx
import { CandlestickChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return <CandlestickChart h={300} data={data} dataKey="date" tickLine="y" gridAxis="x" withXAxis={true} withYAxis={true} withTooltip={true} />;
}
// data.ts
export const data = [
{ date: 'Mar 01', open: 136, high: 142, low: 133, close: 140 },
{ date: 'Mar 02', open: 140, high: 145, low: 138, close: 139 },
{ date: 'Mar 03', open: 139, high: 141, low: 129, close: 131 },
{ date: 'Mar 04', open: 131, high: 134, low: 124, close: 125 },
{ date: 'Mar 05', open: 125, high: 133, low: 124, close: 132 },
{ date: 'Mar 06', open: 132, high: 138, low: 131, close: 137 },
{ date: 'Mar 07', open: 137, high: 137, low: 128, close: 129 },
{ date: 'Mar 08', open: 129, high: 135, low: 127, close: 134 },
{ date: 'Mar 09', open: 134, high: 148, low: 133, close: 146 },
{ date: 'Mar 10', open: 146, high: 152, low: 144, close: 151 },
{ date: 'Mar 11', open: 151, high: 154, low: 143, close: 145 },
{ date: 'Mar 12', open: 145, high: 149, low: 142, close: 148 },
{ date: 'Mar 13', open: 148, high: 156, low: 147, close: 155 },
{ date: 'Mar 14', open: 155, high: 158, low: 150, close: 152 },
{ date: 'Mar 15', open: 152, high: 153, low: 141, close: 143 },
{ date: 'Mar 16', open: 143, high: 147, low: 139, close: 146 },
{ date: 'Mar 17', open: 146, high: 160, low: 145, close: 159 },
{ date: 'Mar 18', open: 159, high: 164, low: 156, close: 157 },
{ date: 'Mar 19', open: 157, high: 162, low: 153, close: 161 },
{ date: 'Mar 20', open: 161, high: 168, low: 160, close: 166 },
];
```
## Charts reference areas
[AreaChart](https://mantine.dev/charts/area-chart), [BarChart](https://mantine.dev/charts/bar-chart), [LineChart](https://mantine.dev/charts/line-chart),
[CompositeChart](https://mantine.dev/charts/composite-chart) and [ScatterChart](https://mantine.dev/charts/scatter-chart) now support the
`referenceAreas` prop that highlights a rectangular region of the plot – a weekend band, a target
range, a threshold zone and similar annotations. Each area is bounded by `x1`/`x2` and/or `y1`/`y2`
data values (omit one pair to span the full opposite axis) and supports a theme `color` and a `label`.
```tsx
// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<AreaChart
h={300}
data={data}
dataKey="date"
type="stacked"
series={[
{ name: 'Apples', color: 'indigo.6' },
{ name: 'Oranges', color: 'blue.6' },
{ name: 'Tomatoes', color: 'teal.6' },
]}
referenceAreas={[
{ x1: 'Mar 23', x2: 'Mar 25', color: 'red.6', label: 'Weekend' },
]}
/>
);
}
// data.ts
export const data = [
{
date: 'Mar 22',
Apples: 2890,
Oranges: 2338,
Tomatoes: 2452,
},
{
date: 'Mar 23',
Apples: 2756,
Oranges: 2103,
Tomatoes: 2402,
},
{
date: 'Mar 24',
Apples: 3322,
Oranges: 986,
Tomatoes: 1821,
},
{
date: 'Mar 25',
Apples: 3470,
Oranges: 2108,
Tomatoes: 2809,
},
{
date: 'Mar 26',
Apples: 3129,
Oranges: 1726,
Tomatoes: 2290,
},
];
```
## Charts reference dots
[AreaChart](https://mantine.dev/charts/area-chart), [BarChart](https://mantine.dev/charts/bar-chart), [LineChart](https://mantine.dev/charts/line-chart),
[CompositeChart](https://mantine.dev/charts/composite-chart) and [ScatterChart](https://mantine.dev/charts/scatter-chart) now support the
`referenceDots` prop that marks individual points on the plot – a peak, an event, a record value or an
anomaly. Each dot is positioned by `x`/`y` data coordinates and supports a radius, a theme `color` and a
`label`. Reference dots are rendered on top of the chart series.
```tsx
// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<AreaChart
h={300}
data={data}
dataKey="date"
series={[
{ name: 'Apples', color: 'indigo.6' },
{ name: 'Oranges', color: 'blue.6' },
{ name: 'Tomatoes', color: 'teal.6' },
]}
referenceDots={[
{ x: 'Mar 25', y: 3470, color: 'red.6', label: 'Peak' },
]}
/>
);
}
// data.ts
export const data = [
{
date: 'Mar 22',
Apples: 2890,
Oranges: 2338,
Tomatoes: 2452,
},
{
date: 'Mar 23',
Apples: 2756,
Oranges: 2103,
Tomatoes: 2402,
},
{
date: 'Mar 24',
Apples: 3322,
Oranges: 986,
Tomatoes: 1821,
},
{
date: 'Mar 25',
Apples: 3470,
Oranges: 2108,
Tomatoes: 2809,
},
{
date: 'Mar 26',
Apples: 3129,
Oranges: 1726,
Tomatoes: 2290,
},
];
```
Note that `referenceLines` in [AreaChart](https://mantine.dev/charts/area-chart) are now rendered on top of the areas
instead of behind them, which makes them consistent with `BarChart`, `LineChart`, `CompositeChart`
and `ScatterChart`, where reference lines were already painted over the series.
## AreaChart streamgraph
[AreaChart](https://mantine.dev/charts/area-chart) now supports `type="stream"` that renders a streamgraph (also known
as ThemeRiver) – a stacked area chart whose baseline flows around a central axis instead of being
fixed to zero, producing the characteristic organic "river" shape. The y-axis is hidden by default
for this type since its floating baseline makes the values not meaningful to read off:
```tsx
// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<AreaChart
h={300}
data={data}
dataKey="month"
type="stream"
series={[
{ name: 'Apples', color: 'indigo.6' },
{ name: 'Oranges', color: 'blue.6' },
{ name: 'Tomatoes', color: 'teal.6' },
{ name: 'Grapes', color: 'grape.6' },
]}
/>
);
}
// data.ts
export const data = [
{ month: 'Jan', Apples: 220, Oranges: 140, Tomatoes: 90, Grapes: 60 },
{ month: 'Feb', Apples: 260, Oranges: 180, Tomatoes: 120, Grapes: 90 },
{ month: 'Mar', Apples: 300, Oranges: 240, Tomatoes: 180, Grapes: 140 },
{ month: 'Apr', Apples: 340, Oranges: 320, Tomatoes: 260, Grapes: 200 },
{ month: 'May', Apples: 380, Oranges: 420, Tomatoes: 360, Grapes: 280 },
{ month: 'Jun', Apples: 420, Oranges: 520, Tomatoes: 460, Grapes: 360 },
{ month: 'Jul', Apples: 400, Oranges: 560, Tomatoes: 520, Grapes: 420 },
{ month: 'Aug', Apples: 360, Oranges: 520, Tomatoes: 560, Grapes: 460 },
{ month: 'Sep', Apples: 300, Oranges: 440, Tomatoes: 520, Grapes: 420 },
{ month: 'Oct', Apples: 260, Oranges: 340, Tomatoes: 440, Grapes: 360 },
{ month: 'Nov', Apples: 220, Oranges: 260, Tomatoes: 340, Grapes: 280 },
{ month: 'Dec', Apples: 200, Oranges: 200, Tomatoes: 260, Grapes: 200 },
];
```
## ScatterChart right Y axis
[ScatterChart](https://mantine.dev/charts/scatter-chart) now supports the `withRightYAxis` prop that displays an
additional Y axis on the right side of the chart, configurable with `rightYAxisProps` and
`rightYAxisLabel`. Bind data series to the right Y axis by setting `yAxisId: 'right'` in the `data`
object – series without `yAxisId` are bound to the left Y axis. Both axes use the same `dataKey.y`
value, but their scales are calculated independently from the series assigned to them:
```tsx
// Demo.tsx
import { ScatterChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<ScatterChart
h={350}
data={data}
dataKey={{ x: 'month', y: 'value' }}
withLegend
withRightYAxis
xAxisLabel="Month"
yAxisLabel="Revenue"
rightYAxisLabel="Conversion rate"
rightYAxisProps={{ unit: '%' }}
/>
);
}
// data.ts
export const data = [
{
color: 'indigo.6',
name: 'Revenue',
data: [
{ month: 1, value: 1200 },
{ month: 2, value: 1400 },
{ month: 3, value: 1350 },
{ month: 4, value: 1800 },
{ month: 5, value: 2100 },
{ month: 6, value: 1950 },
{ month: 7, value: 2400 },
{ month: 8, value: 2650 },
{ month: 9, value: 2300 },
{ month: 10, value: 2800 },
{ month: 11, value: 3100 },
{ month: 12, value: 3400 },
],
},
{
color: 'teal.6',
name: 'Conversion rate',
yAxisId: 'right',
data: [
{ month: 1, value: 3.4 },
{ month: 2, value: 3.9 },
{ month: 3, value: 3.1 },
{ month: 4, value: 4.2 },
{ month: 5, value: 4.8 },
{ month: 6, value: 4.1 },
{ month: 7, value: 5.3 },
{ month: 8, value: 5.9 },
{ month: 9, value: 5.1 },
{ month: 10, value: 6.2 },
{ month: 11, value: 6.8 },
{ month: 12, value: 7.4 },
],
},
];
```
## Stepper labelPosition
[Stepper](https://mantine.dev/core/stepper) component now supports the `labelPosition` prop. Set `labelPosition="bottom"`
to display the step label and description below the step icon:
```tsx
import { useState } from 'react';
import { Stepper } from '@mantine/core';
function Demo() {
const [active, setActive] = useState(1);
return (
<Stepper active={active} onStepClick={setActive} labelPosition="bottom">
<Stepper.Step label="Account" />
<Stepper.Step label="Verification" />
<Stepper.Step label="Access" />
</Stepper>
);
}
```
## Cascader safe area polygon
[Cascader](https://mantine.dev/core/cascader) with `expandTrigger="hover"` now keeps the open column in place while the
cursor moves diagonally toward it – options that the cursor passes over on the way no longer replace
it. Set `safeAreaPolygon={false}` to expand on every hover immediately, or pass an object to configure
[Floating UI `safePolygon`](https://floating-ui.com/docs/useHover#safepolygon) options:
```tsx
import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';
function Demo() {
const withColumns = useMatches({ base: false, sm: true });
return (
<Cascader
withColumns={withColumns}
expandTrigger="hover"
safeAreaPolygon={false}
label="Location"
placeholder="Hover to expand"
data={data}
/>
);
}
```
## YearView renderDay
[YearView](https://mantine.dev/schedule/year-view) now supports the `renderDay` prop that replaces the entire content of
a day cell. The function is called with the day date in `YYYY-MM-DD` format and the events grouped on
that day – the same list that is used to render the default indicators, but without the three items
limit. This makes it possible to display counts, badges or icons instead of the default dots:
```tsx
// Demo.tsx
import dayjs from 'dayjs';
import { YearView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<YearView
date={new Date()}
events={events}
renderDay={(date, dayEvents) => (
<>
{dayjs(date).date()}
{dayEvents.length > 0 && (
<div
style={{
position: 'absolute',
bottom: 0,
insetInlineEnd: 0,
minWidth: 12,
height: 12,
borderRadius: 12,
fontSize: 9,
lineHeight: '12px',
fontWeight: 700,
textAlign: 'center',
color: 'var(--mantine-color-white)',
backgroundColor: `var(--mantine-color-${dayEvents[0].color}-filled)`,
}}
>
{dayEvents.length}
</div>
)}
</>
)}
/>
);
}
```
## ResourcesMonthView event resize
[ResourcesMonthView](https://mantine.dev/schedule/resources-month-view) now supports the `withEventResize` prop. Events
can be resized by dragging their start or end edges, and the `onEventResize` callback is called with
the updated event start and end dates. Resizing snaps to whole days and preserves the event's original
time of day. Use `canResizeEvent` to control which events can be resized:
```tsx
// Demo.tsx
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withEventResize
onEventResize={({ eventId, newStart, newEnd }) => {
setEvents((current) =>
current.map((event) =>
event.id === eventId
? { ...event, start: newStart, end: newEnd }
: event
)
);
}}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}
// data.ts
import dayjs from 'dayjs';
import { ScheduleResourceData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DD');
const nextWeek = dayjs().add(5, 'day').format('YYYY-MM-DD');
const resources: ScheduleResourceData[] = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
{ id: 'new-york', label: 'Meeting room: New York' },
];
const events = [
{
id: 1,
title: 'Team Standup',
start: \`\${today} 09:00:00\`,
end: \`\${today} 09:30:00\`,
color: 'blue',
resourceId: 'tokyo',
},
{
id: 2,
title: 'Sprint Planning',
start: \`\${today} 10:00:00\`,
end: \`\${today} 11:30:00\`,
color: 'green',
resourceId: 'paris',
},
{
id: 3,
title: 'Design Review',
start: \`\${tomorrow} 13:00:00\`,
end: \`\${tomorrow} 14:00:00\`,
color: 'orange',
resourceId: 'tokyo',
},
{
id: 4,
title: 'Client Call',
start: \`\${tomorrow} 09:30:00\`,
end: \`\${tomorrow} 10:30:00\`,
color: 'violet',
resourceId: 'new-york',
},
{
id: 5,
title: 'Workshop',
start: \`\${nextWeek} 14:00:00\`,
end: \`\${nextWeek} 16:00:00\`,
color: 'pink',
resourceId: 'paris',
},
];
```
## Schedule drag and resize intervals
Time-grid Schedule views (`DayView`, `WeekView`, `ResourcesDayView`, `ResourcesWeekView`) now support
`eventDragInterval` and `eventResizeInterval` props that set the snap step used when events are moved
and resized, independent of the `intervalMinutes` grid size. For example, a 30-minute grid can allow
15-minute drag and resize increments. A ghost preview shows where the event will land while dragging:
```tsx
import { useState } from 'react';
import dayjs from 'dayjs';
import { DayView, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Morning Standup',
start: `${today} 09:00:00`,
end: `${today} 09:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Team Meeting',
start: `${today} 11:00:00`,
end: `${today} 12:00:00`,
color: 'green',
},
{
id: 3,
title: 'Code Review',
start: `${today} 14:00:00`,
end: `${today} 15:00:00`,
color: 'violet',
},
];
function Demo() {
const [events, setEvents] = useState(initialEvents);
const handleEventDrop = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
return (
<DayView
date={new Date()}
events={events}
startTime="08:00:00"
endTime="18:00:00"
intervalMinutes={30}
eventDragInterval={15}
withSubHourGridLines={false}
withEventsDragAndDrop
onEventDrop={handleEventDrop}
/>
);
}
```
```tsx
import { useState } from 'react';
import dayjs from 'dayjs';
import { DayView, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Morning Standup',
start: `${today} 09:00:00`,
end: `${today} 09:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Team Meeting',
start: `${today} 11:00:00`,
end: `${today} 12:00:00`,
color: 'green',
},
{
id: 3,
title: 'Code Review',
start: `${today} 14:00:00`,
end: `${today} 15:00:00`,
color: 'violet',
},
];
function Demo() {
const [events, setEvents] = useState(initialEvents);
const handleEventResize = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
return (
<DayView
date={new Date()}
events={events}
startTime="08:00:00"
endTime="18:00:00"
intervalMinutes={30}
eventResizeInterval={15}
withSubHourGridLines={false}
withEventResize
onEventResize={handleEventResize}
/>
);
}
```
## Dropzone react-dropzone 20
[Dropzone](https://mantine.dev/x/dropzone) now depends on `react-dropzone` 20 (previously 15). The upgrade brings several
behavior and type changes:
- `maxFiles` no longer rejects the entire batch when more files are picked than the limit allows.
Files up to the limit are now accepted and the rest are rejected. For example, picking 3 files with
`maxFiles={2}` calls `onDrop` with the first 2 files and `onReject` with the third – previously all 3
files were rejected and `onDrop` was called with an empty array.
- `FileWithPath` type now has required `path` and `relativePath` properties, they were optional
before. The default file aggregator always sets both values, so `onDrop` files can be read without
optional chaining. If you provide a custom `getFilesFromEvent` that returns plain `File` objects,
these properties are not set at runtime.
- `getFilesFromEvent` prop now receives `DropEvent | FileSystemFileHandle[]` instead of `DropEvent` –
the File System Access API path passes file handles to the aggregator. Update the parameter type of
custom aggregators to accept both.
- `react-dropzone` 20 requires Node.js 22 or later. Mantine now requires Node.js 22 as well –
Node.js 20 reached end of life in April 2026. This affects your development environment only,
browser support is not changed.
## Other changes
- [ColorInput](https://mantine.dev/core/color-input) now supports `fullWidth` prop: the dropdown matches the width of the input and the color picker inside it fills the available space.
- [FloatingWindow](https://mantine.dev/core/floating-window) now supports `onSizeChange`, `onResizeStart` and `onResizeEnd` callbacks that mirror `onPositionChange`, `onDragStart` and `onDragEnd` used for dragging. Sizes passed to `onSizeChange` are measured after the new size has been applied, so they are already clamped by the `dimensions` and viewport constraints.
- [PasswordInput](https://mantine.dev/core/password-input) now supports `visibilityToggleFocusable` prop that puts the visibility toggle in the tab order: the button receives `tabindex="0"` and can be activated with Enter or Space.
- Schedule views (`DayView`, `WeekView`, `MonthView`, `ResourcesDayView`, `ResourcesWeekView`) now support `withInteractiveBackgroundEvents` prop – background events (`display: 'background'`) become clickable and trigger `onEventClick`, which makes it possible to open an edit modal for unavailability blocks and similar events.
- [use-scroll-spy](https://mantine.dev/hooks/use-scroll-spy) hook `scrollHost` option now accepts a ref object in addition to a resolved `HTMLElement` – the hook reads `ref.current` internally once the element is mounted, so the scroll host does not need to exist on the first render.
- [YearView](https://mantine.dev/schedule/year-view) now supports `withWeekendDays` prop. Set `withWeekendDays={false}` to hide weekend days – every month grid shrinks to the remaining columns and events that fall only on hidden days are not displayed.