9.2.2
[@mantine/core]Pill: Fix incorrect overflow handling (#8929)[@mantine/dates]TimePicker: Fix incorrect am/pm switching in some cases in production builds (#8911)[@mantine/hooks]use-mask: Fix undo keyboard shortcut not working (#8927)[@mantine/hooks]use-mask: Fix cursor jumping on paste/cut (#8926)[@mantine/core]Input: Fix sections misplaced whendiroverrides parent direction (#8905)[@mantine/core]Select: Fix clear button not showing for falsy primitive values (#8901)[@mantine/core]Fix incorrect attributes type in Modal, Drawer and Spotlight[@mantine/tiptap]Fix controls throwing errors when editor is destroyed/not initialized (#8900)[@mantine/core]Menu: Add option to pass safe area polygon options down to Menu.Sub (#8908)
- @chbaefront made their first contribution in https://github.com/mantinedev/mantine/pull/8908
- @krusche made their first contribution in https://github.com/mantinedev/mantine/pull/8900
- @hyeongjun6364 made their first contribution in https://github.com/mantinedev/mantine/pull/8901
- @Israadaassi1 made their first contribution in https://github.com/mantinedev/mantine/pull/8921
Full Changelog: https://github.com/mantinedev/mantine/compare/9.2.1...9.2.2
9.2.1
[@mantine/tiptap]Fix controls having stale state when built with react compiler (#8725)[@mantine/charts]Fix highlighted are being stuck at the previously hovered chart legend section if mouse is moved quickly (#8768)[@mantine/modals]Fix incorrect duplicate modals ids handling (#8736)[@mantine/core]Table: Fix th borders being rendered transparent ifstickyprop set (#8778)[@mantine/core]Fix error id not being passed toaria-describedbyin Checkbox, Radio and Switch components (#8820)[@mantine/core]Addaria-valuetextsupport to Slider and RangeSlider (#8871)[@mantine/schedule]MonthView: Improve multi-day events overlap rendering for maxed-out days (#8874)[@mantine/core]FixmergeMantineThememutatedDEFAULT_THEME.headings(#8875)[@mantine/hooks]use-debounced-value: Fixleadingcallback not being reset on timeout (#8833)[@mantine/core]Highlight: Add accent insensitive option support (#8890)[@mantine/form]Fix some handlers not being stable reference (#8891)[@mantine/dropzone]ChangeuseFsAccessApito false by default to make Dropzone compatible with all current browsers (#8876)[@mantine/schedule]Fix incorrect events positioning withintervalMinutes={60}(#8887)[@mantine/core]PinInput: Fix keyboard shorcuts being blocked on numeric input type (#8889)[@mantine/form]Fix default validators making form.validate return value async (#8880)[@mantine/core]Menu: Add safe polygon support for sub menus (#8888)[@mantine/core]ScrollArea: Fix Maximum update depth exceeded error[@mantine/core]TreeSelect: Fix focus to moving to input after clear button click
- @oab24413gmai made their first contribution in https://github.com/mantinedev/mantine/pull/8892
- @sarioglu made their first contribution in https://github.com/mantinedev/mantine/pull/8890
- @noahsilas made their first contribution in https://github.com/mantinedev/mantine/pull/8833
- @chaitanya-bhagavan made their first contribution in https://github.com/mantinedev/mantine/pull/8875
- @liamdon made their first contribution in https://github.com/mantinedev/mantine/pull/8874
- @oozan made their first contribution in https://github.com/mantinedev/mantine/pull/8820
- @Pirulax made their first contribution in https://github.com/mantinedev/mantine/pull/8736
Full Changelog: https://github.com/mantinedev/mantine/compare/9.2.0...9.2.1
9.2.0 🔥
View changelog with demos on mantine.dev website
You can now sponsor Mantine development with OpenCollective. All funds are used to improve Mantine and create new features and components.
New TreeSelect component allows picking one or more values from hierarchical tree data. It supports three selection modes: single, multiple, and checkbox (with parent-child cascade):
import { TreeSelect } from '@mantine/core';
import { data } from './data';
function Demo() {
return (
<TreeSelect
label="Your favorite item"
placeholder="Pick value"
data={data}
/>
);
}
New Combobox examples showing how to build tree select components from Combobox primitives with connecting lines, expand/collapse chevrons, and proper indentation:
- Tree select – basic single-value tree select
- Tree multi select – multi select with checkbox cascade
- Searchable tree select – tree select with search filtering
- Tree select with checkboxes – single select with expand-on-click
- Virtualized tree select – large tree (~500 nodes) with @tanstack/react-virtual
@mantine/notifications now supports dismissing notifications by dragging them left or right, and with horizontal scroll swipe while hovered. Both interactions can be disabled on Notifications, and individual items can opt out with allowClose: false.
import { Button } from '@mantine/core';
import { notifications } from '@mantine/notifications';
function Demo() {
return (
<Button
onClick={() =>
notifications.show({
title: 'Default notification',
message: 'Do not forget to star Mantine on GitHub! 🌟',
})
}
>
Show notification
</Button>
);
}
New use-drag hook handles pointer drag gestures with movement tracking, velocity, direction and axis constraints. It uses the Pointer Events API and works with both mouse and touch input:
import { useState } from 'react';
import { Button, Group, Paper, Text } from '@mantine/core';
import { useDrag } from '@mantine/hooks';
interface NotificationItem {
id: number;
text: string;
}
function SwipeNotification({
notification,
onDismiss,
}: {
notification: NotificationItem;
onDismiss: (id: number) => void;
}) {
const [offset, setOffset] = useState(0);
const [dismissed, setDismissed] = useState(false);
const { ref, active } = useDrag(
(state) => {
if (state.last) {
const shouldDismiss =
Math.abs(state.movement[0]) > 120 || state.velocity[0] > 0.5;
if (shouldDismiss) {
setDismissed(true);
setTimeout(() => onDismiss(notification.id), 300);
} else {
setOffset(0);
}
} else {
setOffset(state.movement[0]);
}
},
{ axis: 'x', threshold: 5, filterTaps: true }
);
return (
<Paper
ref={ref}
p="sm"
mb="xs"
withBorder
radius="md"
style={{
transform: dismissed
? `translateX(${offset > 0 ? 400 : -400}px)`
: `translateX(${offset}px)`,
opacity: dismissed ? 0 : 1 - Math.min(Math.abs(offset) / 200, 1) * 0.6,
transition: active ? 'none' : 'transform 300ms ease, opacity 300ms ease',
cursor: active ? 'grabbing' : 'grab',
touchAction: 'pan-y',
userSelect: 'none',
}}
>
{notification.text}
</Paper>
);
}
const initialItems: NotificationItem[] = [
{ id: 1, text: 'New message from Alice' },
{ id: 2, text: 'Build succeeded' },
{ id: 3, text: 'Deployment complete' },
{ id: 4, text: 'Review requested' },
];
function Demo() {
const [notifications, setNotifications] = useState(initialItems);
return (
<div style={{ height: 300 }}>
{notifications.map((n) => (
<SwipeNotification
key={n.id}
notification={n}
onDismiss={(id) =>
setNotifications((items) => items.filter((item) => item.id !== id))
}
/>
))}
{notifications.length === 0 && (
<Text ta="center" c="dimmed" py="md">All cleared!</Text>
)}
<Group justify="center" mt="md">
<Button onClick={() => setNotifications(initialItems)}>
Reset
</Button>
</Group>
</div>
);
}
New InlineDateTimePicker component renders a calendar with a time picker inline, without a dropdown. It supports both default and range modes:
import { InlineDateTimePicker } from '@mantine/dates';
function Demo() {
return <InlineDateTimePicker />;
}
Set type="range" to select a date and time range with two time inputs:
import { InlineDateTimePicker } from '@mantine/dates';
function Demo() {
return <InlineDateTimePicker type="range" />;
}
DateTimePicker now supports type="range" to select a date and time range. In range mode, two time inputs are displayed in the dropdown for start and end times:
import { DateTimePicker } from '@mantine/dates';
function Demo() {
return (
<DateTimePicker
type="range"
label="Pick dates and times range"
placeholder="Pick dates and times range"
/>
);
}
DateTimePicker valueFormat prop now accepts a function in addition to a dayjs format string. The callback receives the value as a YYYY-MM-DD HH:mm:ss string and returns the formatted value, which is useful for cases that cannot be expressed with a dayjs format string:
import dayjs from 'dayjs';
import { DateTimePicker } from '@mantine/dates';
function Demo() {
return (
<DateTimePicker
valueFormat={(date) => dayjs(date).format('dddd, MMMM D [at] h:mm A')}
defaultValue="2024-04-11 14:45:00"
label="Pick date and time"
placeholder="Pick date and time"
/>
);
}
New RollingNumber component animates value changes with rolling digit transitions. Each digit independently rolls to its new position when the value changes:
import { useState } from 'react';
import { Button, Group, RollingNumber } from '@mantine/core';
function Demo() {
const [value, setValue] = useState(1234);
return (
<>
<RollingNumber value={value} fz="36px" />
<Group mt="md">
<Button onClick={() => setValue((v) => v + 1)}>Increment</Button>
<Button onClick={() => setValue((v) => v - 1)}>Decrement</Button>
<Button onClick={() => setValue(Math.floor(Math.random() * 10000))}>Random</Button>
</Group>
</>
);
}
MaskInput now supports a resetRef prop that assigns a function that clears the input value imperatively. This is useful because MaskInput is uncontrolled internally, so setting value from a parent does not clear it:
import { useRef } from 'react';
import { MaskInput, Button, Group } from '@mantine/core';
function Demo() {
const resetRef = useRef<() => void>(null);
return (
<>
<MaskInput
label="Phone number"
placeholder="(___) ___-____"
mask="(999) 999-9999"
resetRef={resetRef}
/>
<Group mt="md">
<Button onClick={() => resetRef.current?.()}>Reset</Button>
</Group>
</>
);
}
MaskInput integration with use-form is now documented. Use defaultValue to seed the initial value and onChangeRaw to write the raw value to form state:
import { Button, MaskInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { phone: '' },
});
return (
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<MaskInput
mask="(999) 999-9999"
placeholder="(___) ___-____"
label="Phone"
onChangeRaw={(raw) => form.setFieldValue('phone', raw)}
/>
<Button type="submit" mt="md">
Submit
</Button>
</form>
);
}
New SankeyChart component visualizes flow between nodes as a Sankey diagram where the width of each link is proportional to the flow value:
// Demo.tsx
import { SankeyChart } from '@mantine/charts';
import { data } from './data';
function Demo() {
return <SankeyChart data={data} />;
}
// data.ts
export const data = {
nodes: [
{ name: 'Visit' },
{ name: 'Direct-Favourite' },
{ name: 'Page-Click' },
{ name: 'Detail-Favourite' },
{ name: 'Lost' },
],
links: [
{ source: 0, target: 1, value: 3728.3 },
{ source: 0, target: 2, value: 354170 },
{ source: 2, target: 3, value: 62429 },
{ source: 2, target: 4, value: 291741 },
],
};
MultiSelect and TagsInput now support reordering selected pills. Set the new withPillsReorder prop to enable it. Pills can be reordered with a mouse (drag-and-drop) or keyboard:
- Pills are not part of the
Taborder.ArrowLeftfrom the input (caret at start) moves focus to the last pill. ArrowLeftandArrowRightnavigate between pills (RTL-aware).ArrowRighton the last pill returns focus to the input.Alt + ArrowLeftandAlt + ArrowRightreorder the focused pill (RTL-aware). Focus follows the moved pill so chained moves work.
Reordering is automatically disabled when disabled or readOnly is set. Custom pill renderers receive a reorderProps payload that can be spread onto the pill element to keep reordering working:
import { useState } from 'react';
import { MultiSelect } from '@mantine/core';
function Demo() {
const [value, setValue] = useState(['React', 'Angular', 'Vue']);
return (
<MultiSelect
label="Drag pills to reorder"
description="Selected values can be reordered by dragging pills"
placeholder="Pick value"
data={['React', 'Angular', 'Vue', 'Svelte', 'Solid', 'Ember']}
value={value}
onChange={setValue}
withPillsReorder
/>
);
}
Tree component now supports restricting drop targets with the new allowDrop prop. The callback receives { draggedNode, targetNode, position } and returning false hides the drop indicator and rejects the drop, so users get proper visual feedback before releasing:
import { useState } from 'react';
import { CaretDownIcon } from '@phosphor-icons/react';
import { Group, moveTreeNode, RenderTreeNodePayload, Tree, TreeNodeData } from '@mantine/core';
const data: TreeNodeData[] = [
{
label: 'Pages',
value: 'pages',
children: [
{ label: 'index.tsx', value: 'pages/index.tsx' },
{ label: 'about.tsx', value: 'pages/about.tsx' },
],
},
{
label: 'Components (locked)',
value: 'components',
children: [
{ label: 'Header.tsx', value: 'components/Header.tsx' },
{ label: 'Footer.tsx', value: 'components/Footer.tsx' },
],
},
{ label: 'package.json', value: 'package.json' },
];
function Leaf({ node, expanded, hasChildren, elementProps }: RenderTreeNodePayload) {
return (
<Group gap={5} {...elementProps}>
{hasChildren && (
<CaretDownIcon
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<span>{node.label}</span>
</Group>
);
}
function Demo() {
const [treeData, setTreeData] = useState(data);
return (
<Tree
data={treeData}
// Forbid dropping into or onto "components" branch
allowDrop={({ draggedNode, targetNode, position }) => {
if (draggedNode === 'components' || draggedNode.startsWith('components/')) {
return false;
}
if (targetNode === 'components' && position === 'inside') {
return false;
}
return !targetNode.startsWith('components/');
}}
onDragDrop={(payload) =>
setTreeData((current) => moveTreeNode(current, payload))
}
renderNode={(payload) => <Leaf {...payload} />}
/>
);
}
Tree component now supports restricting drag initiation to a dedicated handle with the new withDragHandle prop. The handle spreads dragHandleProps from the renderNode payload. This is useful when a node contains interactive controls (inputs, buttons) that would otherwise interfere with dragging:
import { useState } from 'react';
import { CaretDownIcon, DotsSixVerticalIcon } from '@phosphor-icons/react';
import { Group, moveTreeNode, RenderTreeNodePayload, Tree, TreeNodeData } from '@mantine/core';
const data: TreeNodeData[] = [
{
label: 'Pages',
value: 'pages',
children: [
{ label: 'index.tsx', value: 'pages/index.tsx' },
{ label: 'about.tsx', value: 'pages/about.tsx' },
],
},
{
label: 'Components',
value: 'components',
children: [
{ label: 'Header.tsx', value: 'components/Header.tsx' },
{ label: 'Footer.tsx', value: 'components/Footer.tsx' },
],
},
{ label: 'package.json', value: 'package.json' },
];
function Leaf({ node, expanded, hasChildren, elementProps, dragHandleProps }: RenderTreeNodePayload) {
return (
<Group gap={4} {...elementProps}>
<DotsSixVerticalIcon
{...dragHandleProps}
size={16}
style={{ cursor: 'grab' }}
/>
{hasChildren && (
<CaretDownIcon
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<span>{node.label}</span>
</Group>
);
}
function Demo() {
const [treeData, setTreeData] = useState(data);
return (
<Tree
data={treeData}
withDragHandle
onDragDrop={(payload) =>
setTreeData((current) => moveTreeNode(current, payload))
}
renderNode={(payload) => <Leaf {...payload} />}
/>
);
}
Default props set on Input and Input.Wrapper in theme.components now cascade to every component built on top of them (TextInput, Textarea, NumberInput, Select, DateInput, and others). This makes it possible to apply shared size, radius, variant, withAsterisk and other props to all inputs at once, while still overriding individual components with their own default props:
import { TextInput, NumberInput, NativeSelect, MantineProvider, createTheme, Input } from '@mantine/core';
const theme = createTheme({
components: {
Input: Input.extend({
defaultProps: {
size: 'md',
radius: 'md',
},
}),
InputWrapper: Input.Wrapper.extend({
defaultProps: {
withAsterisk: true,
},
}),
NumberInput: NumberInput.extend({
defaultProps: {
size: 'lg',
},
}),
},
});
function Demo() {
return (
<MantineProvider theme={theme}>
<TextInput label="Text input" placeholder="Inherits size and radius from Input" />
<NativeSelect
mt="md"
label="Native select"
data={['React', 'Angular', 'Vue', 'Svelte']}
/>
<NumberInput mt="md" label="Number input" placeholder="Overrides shared size with lg" />
</MantineProvider>
);
}
WeekView businessHours prop now accepts a per-day object keyed by day of the week (0 – Sunday, 6 – Saturday) in addition to the shared [start, end] tuple. Days missing from the object or set to null are rendered as fully outside business hours, making it easy to model partial workdays and non-working days:
import { WeekView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<WeekView
date={new Date()}
events={events}
highlightBusinessHours
businessHours={{
1: ['09:00:00', '17:00:00'],
2: ['09:00:00', '17:00:00'],
3: ['09:00:00', '17:00:00'],
4: ['09:00:00', '17:00:00'],
5: ['09:00:00', '13:00:00'],
}}
startTime="07:00:00"
endTime="20:00:00"
/>
);
}
9.1.1
[@mantine/spotlight]Fix error thrown when listId is empty (#8863)[@mantine/schedule]FixonEventClicknot being passed down to MoreEvents in DayView and MonthView components (#8862)[@mantine/core]Tree: Add dnd handle and dnd lock support[@mantine/schedule]Fixlabelsnot propagating to custom schedule header and more events lists[@mantine/modals]Add ModalsSettings type export[@mantine/schedule]FixrenderEventnot working in MoreEvents[@mantine/mcp-server]Update displayed version[@mantine/core]Combobox: Fix keyboard events not triggering in Safari after click on the inout (#7386)[@mantine/hooks]use-focus-return: Fix incorrect logic when used with nested focus traps (#8857)[@mantine/core]ScrollArea: Fix scrollbar never visible withoffsetScrollbars="present"(#8844)[@mantine/core]Fix incorrectrenderOptiontype in Combobox-based components (#8858)[@mantine/hooks]use-mask: Fix stale mask partial remaining as input value on blur after input field was cleared[@mantine/hooks]use-mask: Fix incorrect cursor position handling[@mantine/hooks]use-mask: Fix part of the mask remaining as input value on blur[@mantine/core]Radio: Fix icon not being centered on some low-density screens (#8845)[@mantine/core]Highlight: Fix wholeWord matching for non-ASCII characters[@mantine/core]Card: Fix Card.Section not being handled correctly during server-side rendering of server components (#8846))[@mantine/core]FixclearButtonProps={{ size: lg }}not working when passed to Select and other similar components (#8855)[@mantine/core]Fix Styles API defined for Input not being applied to Select and MultiSelect components (#8851)
- @malekmimouna made their first contribution in https://github.com/mantinedev/mantine/pull/8856
- @dfedoryshchev made their first contribution in https://github.com/mantinedev/mantine/pull/8853
- @stasyzon made their first contribution in https://github.com/mantinedev/mantine/pull/8845
- @atinary-ybouzonie made their first contribution in https://github.com/mantinedev/mantine/pull/8863
Full Changelog: https://github.com/mantinedev/mantine/compare/9.1.0...9.1.1
9.1.0
View changelog with demos on mantine.dev website
You can now sponsor Mantine development with OpenCollective. All funds are used to improve Mantine and create new features and components.
New deduplicateInlineStyles prop on MantineProvider enables React 19 style tag deduplication for responsive style props. When many components share the same responsive style prop values, only a single <style /> tag is generated and hoisted to <head /> instead of each component injecting its own:
import { MantineProvider } from '@mantine/core';
function Demo() {
return (
<MantineProvider deduplicateInlineStyles>
{/* Your app here */}
</MantineProvider>
);
}
This can significantly improve performance when rendering large lists of components with identical responsive style props. See the styles performance guide for more details.
New use-mask hook attaches real-time input masking to any <input> element via a ref callback. It formats user input against a defined pattern and exposes both the masked display value and the raw unmasked value. The hook supports built-in and custom tokens, dynamic masks, character transforms, optional segments, and regex array format:
import { TextInput, Text } from '@mantine/core';
import { useMask } from '@mantine/hooks';
function Demo() {
const { ref, value, rawValue } = useMask({ mask: '(999) 999-9999' });
return (
<>
<TextInput ref={ref} label="Phone number" placeholder="(___) ___-____" />
<Text size="sm" mt="sm">Masked value: {value}</Text>
<Text size="sm">Raw value: {rawValue}</Text>
</>
);
}
New MaskInput component is a wrapper around use-mask hook that provides all standard input props (label, description, error, etc.) and supports all mask options:
import { MaskInput } from '@mantine/core';
function Demo() {
return (
<MaskInput
variant="default" size="sm" radius="md" label="Input label" withAsterisk={false} description="Input description" error=""
mask="(999) 999-9999"
placeholder="(___) ___-____"
/>
);
}
New Treemap component displays hierarchical data as a set of nested rectangles. It is based on the Treemap recharts component:
// Demo.tsx
import { Treemap } from '@mantine/charts';
import { data } from './data';
function Demo() {
return <Treemap data={data} />;
}
// data.ts
export const data = [
{
name: 'Frontend',
color: 'blue.8',
children: [
{ name: 'React', value: 400 },
{ name: 'Vue', value: 200 },
{ name: 'Angular', value: 150 },
],
},
{
name: 'Backend',
color: 'teal.8',
children: [
{ name: 'Node', value: 300 },
{ name: 'Python', value: 250 },
{ name: 'Go', value: 100 },
],
},
{
name: 'Mobile',
color: 'red.8',
children: [
{ name: 'React Native', value: 200 },
{ name: 'Flutter', value: 180 },
],
},
];
TimePicker component now supports type="duration" prop that allows entering durations that exceed 24 hours. In this mode, the hours field has no upper limit and the input width adjusts dynamically based on the entered value:
import { TimePicker } from '@mantine/dates';
function Demo() {
return <TimePicker label="Enter duration" type="duration" withSeconds />;
}
Heatmap component now supports withLegend prop that displays a color legend below the chart. Use legendLabels prop to customize labels (default: ['Less', 'More']):
// Demo.tsx
import { Heatmap } from '@mantine/charts';
import { data } from './data';
function Demo() {
return (
<Heatmap
data={data}
startDate="2024-02-16"
endDate="2025-02-16"
withMonthLabels
withWeekdayLabels
withLegend
/>
);
}
// data.ts
export const data = ${JSON.stringify(data, null, 2)};
MonthPicker and YearPicker components now support presets prop that allows adding predefined values to pick from. Presets are also available in MonthPickerInput and YearPickerInput components:
import dayjs from 'dayjs';
import { MonthPicker } from '@mantine/dates';
function Demo() {
return (
<MonthPicker
presets={[
{ value: dayjs().startOf('month').format('YYYY-MM-DD'), label: 'This month' },
{ value: dayjs().add(1, 'month').startOf('month').format('YYYY-MM-DD'), label: 'Next month' },
{ value: dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD'), label: 'Last month' },
{ value: dayjs().add(6, 'month').startOf('month').format('YYYY-MM-DD'), label: 'In 6 months' },
{ value: dayjs().add(1, 'year').startOf('month').format('YYYY-MM-DD'), label: 'Next year' },
{ value: dayjs().subtract(1, 'year').startOf('month').format('YYYY-MM-DD'), label: 'Last year' },
]}
/>
);
}
New use-roving-index hook implements the roving tabindex keyboard navigation pattern. It manages tabIndex state for a group of focusable elements, handles arrow key navigation with disabled item skipping, and supports both 1D lists and 2D grids:
import { Button, Group } from '@mantine/core';
import { useRovingIndex } from '@mantine/hooks';
const items = ['Bold', 'Italic', 'Underline', 'Strikethrough', 'Code'];
function Demo() {
const { getItemProps } = useRovingIndex({
total: items.length,
orientation: 'horizontal',
loop: true,
});
return (
<Group gap="xs">
{items.map((item, index) => (
<Button key={item} variant="default" {...getItemProps({ index })}>
{item}
</Button>
))}
</Group>
);
}
Tree component now supports drag-and-drop reordering of nodes. Provide onDragDrop callback to enable it, and use the moveTreeNode utility to update data based on the result:
import { useState } from 'react';
import { CaretDownIcon } from '@phosphor-icons/react';
import { Group, moveTreeNode, RenderTreeNodePayload, Tree, TreeNodeData } from '@mantine/core';
const data: TreeNodeData[] = [
{
label: 'Pages',
value: 'pages',
children: [
{ label: 'index.tsx', value: 'pages/index.tsx' },
{ label: 'about.tsx', value: 'pages/about.tsx' },
{ label: 'contact.tsx', value: 'pages/contact.tsx' },
],
},
{
label: 'Components',
value: 'components',
children: [
{ label: 'Header.tsx', value: 'components/Header.tsx' },
{ label: 'Footer.tsx', value: 'components/Footer.tsx' },
{ label: 'Sidebar.tsx', value: 'components/Sidebar.tsx' },
],
},
{ label: 'package.json', value: 'package.json' },
{ label: 'tsconfig.json', value: 'tsconfig.json' },
];
function Leaf({ node, expanded, hasChildren, elementProps }: RenderTreeNodePayload) {
return (
<Group gap={5} {...elementProps}>
{hasChildren && (
<CaretDownIcon
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<span>{node.label}</span>
</Group>
);
}
function Demo() {
const [treeData, setTreeData] = useState(data);
return (
<Tree
data={treeData}
onDragDrop={(payload) =>
setTreeData((current) => moveTreeNode(current, payload))
}
renderNode={(payload) => <Leaf {...payload} />}
/>
);
}
Tree now supports lazy loading of children. Set hasChildren: true on a node without providing children – when the node is expanded for the first time, onLoadChildren callback passed to useTree is called. Use mergeAsyncChildren utility to splice loaded children into your data:
import { useState } from 'react';
import { CaretDownIcon, SpinnerIcon } from '@phosphor-icons/react';
import {
Group,
mergeAsyncChildren,
RenderTreeNodePayload,
Tree,
TreeNodeData,
useTree,
} from '@mantine/core';
const initialData: TreeNodeData[] = [
{ label: 'Documents', value: 'documents', hasChildren: true },
{ label: 'Photos', value: 'photos', hasChildren: true },
{ label: 'README.md', value: 'readme' },
];
// Simulates an API call to load children
async function fetchChildren(parentValue: string): Promise<TreeNodeData[]> {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [
{ label: `${parentValue}/file-1.txt`, value: `${parentValue}/file-1.txt` },
{ label: `${parentValue}/file-2.txt`, value: `${parentValue}/file-2.txt` },
{
label: `${parentValue}/subfolder`,
value: `${parentValue}/subfolder`,
hasChildren: true,
},
];
}
function Leaf({ node, expanded, hasChildren, elementProps, isLoading }: RenderTreeNodePayload) {
return (
<Group gap={5} wrap="nowrap" {...elementProps}>
{isLoading ? (
<SpinnerIcon size={18} style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }} />
) : (
hasChildren && (
<CaretDownIcon
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)', flexShrink: 0 }}
/>
)
)}
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{node.label}
</span>
</Group>
);
}
function Demo() {
const [data, setData] = useState(initialData);
const tree = useTree({
onLoadChildren: async (value) => {
const children = await fetchChildren(value);
setData((prev) => mergeAsyncChildren(prev, value, children));
},
});
return (
<Tree
data={data}
tree={tree}
renderNode={(payload) => <Leaf {...payload} />}
/>
);
}
Tree now includes filterTreeData utility to filter tree data based on a search query. Matching nodes and their ancestors are preserved in the result. You can provide a custom filter function for advanced matching (for example, fuzzy search with fuse.js):
import { useMemo, useState } from 'react';
import {
filterTreeData,
getTreeExpandedState,
TextInput,
Tree,
TreeNodeData,
useTree,
} from '@mantine/core';
const data: TreeNodeData[] = [
{
label: 'src',
value: 'src',
children: [
{
label: 'components',
value: 'src/components',
children: [
{ label: 'Accordion.tsx', value: 'src/components/Accordion.tsx' },
{ label: 'Tree.tsx', value: 'src/components/Tree.tsx' },
{ label: 'Button.tsx', value: 'src/components/Button.tsx' },
{ label: 'Input.tsx', value: 'src/components/Input.tsx' },
],
},
{
label: 'hooks',
value: 'src/hooks',
children: [
{ label: 'use-debounce.ts', value: 'src/hooks/use-debounce.ts' },
{ label: 'use-media-query.ts', value: 'src/hooks/use-media-query.ts' },
],
},
],
},
{
label: 'public',
value: 'public',
children: [
{ label: 'favicon.ico', value: 'public/favicon.ico' },
{ label: 'logo.svg', value: 'public/logo.svg' },
],
},
{ label: 'package.json', value: 'package.json' },
{ label: 'tsconfig.json', value: 'tsconfig.json' },
];
function Demo() {
const [search, setSearch] = useState('');
const tree = useTree();
const filteredData = useMemo(
() => filterTreeData(data, search),
[search]
);
const handleSearchChange = (value: string) => {
setSearch(value);
if (value.trim()) {
const next = filterTreeData(data, value);
tree.setExpandedState(getTreeExpandedState(next, '*'));
} else {
tree.collapseAllNodes();
}
};
return (
<div>
<TextInput
placeholder="Search..."
mb="sm"
value={search}
onChange={(event) => handleSearchChange(event.currentTarget.value)}
/>
<Tree data={filteredData} tree={tree} />
</div>
);
}
Tree now supports withLines prop to display connecting lines showing parent-child relationships. Lines adapt to levelOffset spacing automatically:
import { getTreeExpandedState, Tree, useTree } from '@mantine/core';
import { data } from './data';
function Demo() {
const tree = useTree({
initialExpandedState: getTreeExpandedState(data, '*'),
});
return <Tree data={data} tree={tree} withLines />;
}
Tree now provides flattenTreeData utility and FlatTreeNode component for virtualized rendering of large trees. The component does not depend on any virtualization library – you supply one yourself (e.g., @tanstack/react-virtual):
import { useMemo, useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import {
FlatTreeNode,
flattenTreeData,
getTreeExpandedState,
TreeNodeData,
useTree,
} from '@mantine/core';
const ITEM_HEIGHT = 30;
function generateTreeData(count: number): TreeNodeData[] {
const result: TreeNodeData[] = [];
let id = 0;
function addChildren(
parent: TreeNodeData[],
depth: number,
remaining: { count: number }
) {
const childCount = depth === 0 ? 20 : Math.floor(Math.random() * 8) + 2;
for (let i = 0; i < childCount && remaining.count > 0; i++) {
id++;
remaining.count--;
const hasChild =
depth < 3 && remaining.count > 0 && Math.random() > 0.3;
const node: TreeNodeData = {
label: `${hasChild ? 'Folder' : 'File'} ${id}`,
value: `node-${id}`,
children: hasChild ? [] : undefined,
};
if (hasChild) {
addChildren(node.children!, depth + 1, remaining);
}
parent.push(node);
}
}
addChildren(result, 0, { count });
return result;
}
const largeData = generateTreeData(2000);
const initialExpandedState = getTreeExpandedState(largeData, '*');
function Demo() {
const tree = useTree({
initialExpandedState,
});
const flatList = useMemo(
() => flattenTreeData(largeData, tree.expandedState),
[tree.expandedState]
);
const scrollParentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: flatList.length,
getScrollElement: () => scrollParentRef.current,
estimateSize: () => ITEM_HEIGHT,
overscan: 20,
});
return (
<div ref={scrollParentRef} style={{ height: 400, overflow: 'auto' }}>
<div
data-tree-root
role="tree"
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<FlatTreeNode
key={flatList[virtualItem.index].node.value}
{...flatList[virtualItem.index]}
tree={tree}
expandOnClick
selectOnClick
tabIndex={virtualItem.index === 0 ? 0 : -1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
/>
))}
</div>
</div>
);
}
useTree hook now supports checkStrictly option. When enabled, checking a parent node does not affect children and vice versa – each node's checked state is fully independent:
import { CaretDownIcon } from '@phosphor-icons/react';
import { Checkbox, Group, RenderTreeNodePayload, Tree, useTree } from '@mantine/core';
import { data } from './data';
const renderTreeNode = ({
node,
expanded,
hasChildren,
elementProps,
tree,
}: RenderTreeNodePayload) => {
const checked = tree.isNodeChecked(node.value);
return (
<Group gap="xs" {...elementProps}>
<Checkbox.Indicator
checked={checked}
onClick={() =>
checked
? tree.uncheckNode(node.value)
: tree.checkNode(node.value)
}
/>
<Group gap={5} onClick={() => tree.toggleExpanded(node.value)}>
<span>{node.label}</span>
{hasChildren && (
<CaretDownIcon
size={14}
style={{
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
}}
/>
)}
</Group>
</Group>
);
};
function Demo() {
const tree = useTree({ checkStrictly: true });
return (
<Tree
data={data}
tree={tree}
levelOffset={23}
expandOnClick={false}
renderNode={renderTreeNode}
/>
);
}
Slider component now supports startPointValue prop that changes the origin of the filled bar. When set, the bar extends from the given value toward the current value – to the left for values below the start point and to the right for values above it:
import { Slider } from '@mantine/core';
function Demo() {
return (
<Slider
startPointValue={0}
min={-100}
max={100}
defaultValue={40}
marks={[
{ value: -100, label: '-100' },
{ value: -50, label: '-50' },
{ value: 0, label: '0' },
{ value: 50, label: '50' },
{ value: 100, label: '100' },
]}
/>
);
}
WeekView component now supports forceCurrentTimeIndicator prop. When set, the current time indicator is displayed on the same day of week even when viewing a different week:
import { WeekView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<WeekView
date="2030-06-10"
events={events}
withCurrentTimeIndicator
forceCurrentTimeIndicator
/>
);
}
New MonthView demo shows how to use renderEvent to visually differentiate all-day and timed events. All-day events render as regular colored bars, while timed events display as a colored dot with the start time and title:
// Demo.tsx
import dayjs from 'dayjs';
import { Box, UnstyledButton } from '@mantine/core';
import { MonthView, ScheduleEventData } from '@mantine/schedule';
function isAllDayEvent(event: ScheduleEventData) {
const start = dayjs(event.start);
const end = dayjs(event.end);
return start.isSame(start.startOf('day')) && end.isSame(end.startOf('day'));
}
const events: ScheduleEventData[] = [/* ...events */];
function Demo() {
return (
<MonthView
date={new Date()}
events={events}
renderEvent={(event, props) => {
if (isAllDayEvent(event)) {
return <UnstyledButton {...props} />;
}
const { children, className, style, ...others } = props;
return (
<UnstyledButton
{...others}
style={{
...style,
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 10,
whiteSpace: 'nowrap',
overflow: 'hidden',
pointerEvents: 'all',
cursor: 'pointer',
paddingInline: 2,
}}
>
<Box
component="span"
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: `var(--event-bg)`,
flexShrink: 0,
}}
/>
<span style={{ width: 28, flexShrink: 0 }}>{dayjs(event.start).format('h:mm')}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{event.title}
</span>
</UnstyledButton>
);
}}
/>
);
}
- Tabs component now supports
keepMountedModeprop that controls how inactive tab panels are hidden whenkeepMountedistrue. SetkeepMountedMode="display-none"to usedisplay: nonestyles instead of the defaultActivitycomponent. - useClickOutside hook now supports
enabledparameter to dynamically enable/disable the listener. The hook also usesevent.composedPath()in bothrefandnodesbranches for consistent Shadow DOM support and correctly ignores clicks on detached DOM nodes in the single-ref mode. - useCounter hook now supports
stepoption to configure increment/decrement step size (default1). - useDebouncedCallback hook now supports
maxWaitoption to guarantee execution within a maximum time window during continuous calls, andisPending()method to check if a debounced call is waiting. - useDebouncedValue hook now returns a
flushmethod to immediately apply the pending debounced value. - useScrollIntoView hook now supports
onScrollCancelcallback that fires when the scroll animation is interrupted by the user, and returns ascrollingboolean to indicate whether a scroll animation is in progress.
9.0.2
[@mantine/schedule]Change default events border-radius to sm[@mantine/dates]DateTimePicker: Fix formatting not working withwithSecondsset ontimePickerPropsonly[@mantine/core]Textarea: Fix error thrown on resize in some cases[@mantine/modals]Fixmodals.closeAll()called from comtext modal causing infinite rerendering[@mantine/tiptap]RichTextEditor: Fix invisible caret in empty task list items[@mantine/schedule]Fix rrule package imports bot being compatible with esm only bundlers[@mantine/schedule]FixonEventClickcalled when event is resizing[@mantine/core]Fix incorrect default colors resolver for custom colors in light variant
- @dkpark10 made their first contribution in https://github.com/mantinedev/mantine/pull/8807
- @sdelpercio made their first contribution in https://github.com/mantinedev/mantine/pull/8826
Full Changelog: https://github.com/mantinedev/mantine/compare/9.0.1...9.0.2
9.0.1
[@mantine/core]LoadingOverlay: Fix double overlay visible with dark color scheme (#8811)[@mantine/core]RingProgress: Add missing viewBox (#8806)[@mantine/core]Input: AddrootRefprop support[@mantine/core]Combobox: FixrefPropnot working onCombobox.Target(#8798)[@mantine/mcp-server]Fix stdio transport to comply with MCP spec (#8792)[@mantine/core]Input: Fixaria-invalid="false"attribute being set (#8785)[@mantine/core]Slider: Fix incorrect orientation inheritance from the parent markup (#8791)[@mantine/core]Fix incorrect default placeholder size in PasswordInput and other components (#8793)[@mantine/core]Badge: Fix text being cut off with some fonts (#8788)[@mantine/hooks]use-scroller: Fix element dynamic resizing not being handled correctly (#8800)[@mantine/core]FixCheckbox.Group,Switch.Group,Radio.GroupandChip.Groupnot working with generic primitive values (#8801)[@mantine/core]Popover: Fix missingwithProps(#8802)[@mantine/core]Accordion: Fix focus ring being cut off (#8797)[@mantine/charts]Add option to fully customize reference lines label (#8790)[@mantine/core]Fixloadingprop not being handled correctly in TagsInput and MultiSelect (#8803)
Full Changelog: https://github.com/mantinedev/mantine/compare/9.0.0...9.0.1
9.0.0 🤩
View changelog with demos on mantine.dev website
This changelog covers breaking changes and new features in Mantine 9.0. To migrate your application to Mantine 9.0, follow 8.x → 9.x migration guide.
Starting from Mantine 9.0, the following dependencies are required:
- React 19.2+ for all
@mantine/*packages - Tiptap 3+ for
@mantine/tiptap(migration guide) - Recharts 3+ for
@mantine/charts(no migration required)
New @mantine/schedule package provides a complete set of calendar scheduling components for React applications. It includes multiple view levels, drag-and-drop event management, and extensive customization options.
Schedule is a unified container component that combines all views with built-in navigation and view switching. Drag events to reschedule them:
import { useState } from 'react';
import dayjs from 'dayjs';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').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} 10:00:00`,
end: `${today} 11:30:00`,
color: 'green',
},
{
id: 3,
title: 'Lunch Break',
start: `${today} 12:00:00`,
end: `${today} 13:00:00`,
color: 'orange',
},
{
id: 4,
title: 'Code Review',
start: `${tomorrow} 14:00:00`,
end: `${tomorrow} 15:00:00`,
color: 'violet',
},
{
id: 5,
title: 'Client Call',
start: `${tomorrow} 15:30:00`,
end: `${tomorrow} 16:30:00`,
color: 'cyan',
},
{
id: 6,
title: 'All Day Conference',
start: `${today} 00:00:00`,
end: dayjs(today).add(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
color: 'red',
},
];
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 (
<Schedule
events={events}
withEventsDragAndDrop
onEventDrop={handleEventDrop}
/>
);
}
DayView displays a single day with configurable time slots, all-day event section, current time indicator, and business hours highlighting. Drag events to reschedule them:
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"
withEventsDragAndDrop
onEventDrop={handleEventDrop}
/>
);
}
WeekView shows a weekly calendar grid with time slots, week numbers, weekend day toggling, and multi-day event spanning. Drag events across days and time slots:
import { useState } from 'react';
import dayjs from 'dayjs';
import { WeekView, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').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: `${tomorrow} 11:00:00`,
end: `${tomorrow} 12:00:00`,
color: 'green',
},
{
id: 3,
title: 'Code Review',
start: `${today} 14:00:00`,
end: `${today} 15:00:00`,
color: 'violet',
},
{
id: 4,
title: 'Company Holiday',
start: dayjs(getStartOfWeek({ date: today, firstDayOfWeek: 1 })).format('YYYY-MM-DD HH:mm:ss'),
end: dayjs(getStartOfWeek({ date: today, firstDayOfWeek: 1 }))
.add(2, 'day')
.format('YYYY-MM-DD HH:mm:ss'),
color: 'red',
},
{
id: 5,
title: 'Release Day',
start: dayjs(getStartOfWeek({ date: today, firstDayOfWeek: 1 })).format('YYYY-MM-DD HH:mm:ss'),
end: dayjs(getStartOfWeek({ date: today, firstDayOfWeek: 1 }))
.add(2, 'day')
.format('YYYY-MM-DD HH:mm:ss'),
color: 'orange',
},
];
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 (
<WeekView
date={new Date()}
events={events}
startTime="08:00:00"
endTime="18:00:00"
withEventsDragAndDrop
onEventDrop={handleEventDrop}
/>
);
}
MonthView displays a monthly calendar grid with event overflow handling, outside days display, and week numbers. Drag events to different days:
import { useState } from 'react';
import dayjs from 'dayjs';
import { MonthView, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Team Meeting',
start: `${today} 09:00:00`,
end: `${today} 10:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Project Deadline',
start: dayjs().add(5, 'day').format('YYYY-MM-DD 00:00:00'),
end: dayjs().add(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
color: 'red',
},
];
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 <MonthView date={new Date()} events={events} withEventsDragAndDrop onEventDrop={handleEventDrop} />;
}
YearView provides a 12-month year overview organized by quarters with day-level event indicators:
// Demo.tsx
import dayjs from 'dayjs';
import { useState } from 'react';
import { YearView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<YearView
date={date}
onDateChange={setDate}
events={events}
/>
);
}
// data.ts
import dayjs from 'dayjs';
const thisYear = dayjs().format('YYYY');
const events = [
{
id: 1,
title: 'New Year',
start: \\\`\\\${thisYear}-01-01 00:00:00\\\`,
end: dayjs(\\\`\\\${thisYear}-01-01\\\`).add(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
color: 'blue',
},
{
id: 2,
title: 'Spring Event',
start: \\\`\\\${thisYear}-03-15 00:00:00\\\`,
end: dayjs(\\\`\\\${thisYear}-03-15\\\`).add(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
color: 'green',
},
];
MobileMonthView is a mobile-optimized month view with event details panel for the selected day:
// Demo.tsx
import dayjs from 'dayjs';
import { useState } from 'react';
import { MobileMonthView } from '@mantine/schedule';
import { events } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [selectedDate, setSelectedDate] = useState<string | null>(dayjs().format('YYYY-MM-DD'));
return (
<MobileMonthView
date={date}
onDateChange={setDate}
selectedDate={selectedDate}
onSelectedDateChange={setSelectedDate}
events={regularEvents}
/>
);
}
// data.ts
import dayjs from 'dayjs';
const thisMonth = dayjs().format('YYYY-MM');
export const events = [
{
id: 1,
title: 'Team Meeting',
start: \`\${thisMonth}-05 09:00:00\`,
end: \`\${thisMonth}-05 10:00:00\`,
color: 'blue',
},
{
id: 2,
title: 'Project Review',
start: \`\${thisMonth}-05 14:00:00\`,
end: \`\${thisMonth}-05 15:30:00\`,
color: 'green',
},
{
id: 3,
title: 'Conference',
start: \`\${thisMonth}-10 00:00:00\`,
end: \`\${thisMonth}-12 00:00:00\`,
color: 'violet',
},
// ... more events
];
To get started, follow the getting started guide.
Collapse component now supports horizontal orientation:
import { Button, Collapse, Stack, Typography } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [expanded, handlers] = useDisclosure(false);
return (
<Stack h={240} align="flex-start">
<Button onClick={handlers.toggle} w="fit-content">
{expanded ? 'Collapse' : 'Expand'}
</Button>
<Collapse expanded={expanded} orientation="horizontal">
<Typography bg="var(--mantine-color-blue-light)" p="xs" bdrs="md" w={200}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
</Typography>
</Collapse>
</Stack>
);
}
New use-collapse hook is the hook version of Collapse component. It allows animation of height from 0 to auto and vice versa.
import { Button, Typography } from '@mantine/core';
import { useCollapse, useDisclosure } from '@mantine/hooks';
function Demo() {
const [expanded, handlers] = useDisclosure(false);
const getCollapseProps = useCollapse({ expanded });
return (
<>
<Button onClick={handlers.toggle} mb="md">
{expanded ? 'Collapse' : 'Expand'}
</Button>
<div {...getCollapseProps()}>
<Typography bg="var(--mantine-color-blue-light)" p="xs" bdrs="md">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
</Typography>
</div>
</>
);
}
use-horizontal-collapse works the same way as use-collapse but animates width instead of height:
import { Button, Stack, Typography } from '@mantine/core';
import { useDisclosure, useHorizontalCollapse } from '@mantine/hooks';
function Demo() {
const [expanded, handlers] = useDisclosure(false);
const { getCollapseProps } = useHorizontalCollapse({ expanded });
return (
<Stack h={240}>
<Button onClick={handlers.toggle} w="fit-content">
{expanded ? 'Collapse' : 'Expand'}
</Button>
<div {...getCollapseProps({ style: { width: 200 } })}>
<Typography bg="var(--mantine-color-blue-light)" p="xs" bdrs="md" w={200}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
</Typography>
</div>
</Stack>
);
}
New use-floating-window hook allows creating floating draggable elements:
import { Button, CloseButton, Group, Paper, Portal, Text } from '@mantine/core';
import { useDisclosure, useFloatingWindow } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
const floatingWindow = useFloatingWindow({
constrainToViewport: true,
constrainOffset: 20,
excludeDragHandleSelector: 'button',
initialPosition: { top: 300, left: 20 },
});
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<Portal>
<Paper
w={280}
p="md"
withBorder
pos="fixed"
style={{ cursor: 'move', transition: 'box-shadow 70ms ease', zIndex: 400 }}
shadow={floatingWindow.isDragging ? 'md' : undefined}
ref={floatingWindow.ref}
>
<Group justify="space-between" mb="md">
<Text>Usage demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">This is a floating window. You can drag it around.</Text>
</Paper>
</Portal>
)}
</>
);
}
FloatingWindow provides component API for use-floating-window hook:
import { Button, CloseButton, FloatingWindow, Group, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [visible, handlers] = useDisclosure();
return (
<>
<Button onClick={handlers.toggle} variant="default">
{visible ? 'Hide' : 'Show'} floating window
</Button>
{visible && (
<FloatingWindow
w={280}
p="md"
withBorder
excludeDragHandleSelector="button"
initialPosition={{ top: 300, left: 20 }}
style={{ cursor: 'move' }}
>
<Group justify="space-between" mb="md">
<Text>Usage demo</Text>
<CloseButton onClick={handlers.close} />
</Group>
<Text fz="sm">This is a floating window. You can drag it around.</Text>
</FloatingWindow>
)}
</>
);
}
New OverflowList component displays list of items and collapses the overflowing items into a single element:
// OverflowListDemo.tsx
import { Badge, OverflowList } from '@mantine/core';
import { data } from './data';
function Demo() {
return (
<div style={{ resize: 'horizontal', overflow: 'auto', maxWidth: '100%' }}>
<OverflowList
data={data}
gap={4}
renderOverflow={(items) => <Badge>+{items.length} more</Badge>}
renderItem={(item, index) => <Badge key={index}>{item}</Badge>}
/>
</div>
);
}
// data.ts
export const data = [
'Apple',
'Banana',
'Cherry',
'Date',
'Elderberry',
'Fig',
'Grape',
'Honeydew',
'Indian Fig',
'Jackfruit',
'Kiwi',
'Lemon',
'Mango',
'Nectarine',
'Orange',
'Papaya',
];
New Marquee component creates continuous scrolling animation for content:
import { Marquee } from '@mantine/core';
import { MantineLogo } from '@mantinex/mantine-logo';
function Demo() {
return (
<Marquee gap="lg">
<MantineLogo width={80} type="full" color="blue" />
<MantineLogo width={80} type="full" color="cyan" />
<MantineLogo width={80} type="full" color="teal" />
<MantineLogo width={80} type="full" color="green" />
<MantineLogo width={80} type="full" color="lime" />
<MantineLogo width={80} type="full" color="yellow" />
<MantineLogo width={80} type="full" color="orange" />
<MantineLogo width={80} type="full" color="red" />
</Marquee>
);
}
New Scroller component displays horizontally scrollable content with navigation controls. It supports native scrolling via trackpad, shift + mouse wheel, touch gestures, and mouse drag:
import { Badge, Group, Scroller } from '@mantine/core';
function Demo() {
return (
<Scroller>
<Group gap="xs" wrap="nowrap">
{Array.from({ length: 20 }).map((_, index) => (
<Badge key={index} variant="light" size="lg">
Badge {index + 1}
</Badge>
))}
</Group>
</Scroller>
);
}
New use-scroller hook provides logic for creating custom scrollable containers with navigation controls:
import { Box, Button, Group } from '@mantine/core';
import { useScroller } from '@mantine/hooks';
function Demo() {
const scroller = useScroller();
return (
<Box>
<Group mb="md">
<Button
onClick={scroller.scrollStart}
disabled={!scroller.canScrollStart}
variant="default"
size="xs"
>
← Scroll left
</Button>
<Button
onClick={scroller.scrollEnd}
disabled={!scroller.canScrollEnd}
variant="default"
size="xs"
>
Scroll right →
</Button>
</Group>
<div
ref={scroller.ref}
{...scroller.dragHandlers}
style={{
overflow: 'auto',
cursor: scroller.isDragging ? 'grabbing' : 'grab',
}}
>
<Group wrap="nowrap" gap="md">
{Array.from({ length: 20 }).map((_, index) => (
<Box
key={index}
style={{
minWidth: 100,
height: 80,
backgroundColor: 'var(--mantine-color-blue-filled)',
borderRadius: 'var(--mantine-radius-md)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 500,
}}
>
{index + 1}
</Box>
))}
</Group>
</div>
</Box>
);
}
New BarsList component displays a list of horizontal bars with names and values. It supports custom colors, auto contrast, value formatting, and custom bar rendering:
// Demo.tsx
import { BarsList } from '@mantine/charts';
import { data } from './data';
function Demo() {
return <BarsList data={data} />;
}
// data.ts
export const data = ${JSON.stringify(data, null, 2)};
Card component now supports horizontal orientation:
import { Box, Card, Group, RingProgress, Text } from '@mantine/core';
const completed = 1887;
const total = 2334;
const stats = [
{ value: 447, label: 'Remaining' },
{ value: 76, label: 'In progress' },
];
function Demo() {
const items = stats.map((stat) => (
<div key={stat.label}>
<Text>{stat.value}</Text>
<Text size="xs" c="dimmed">
{stat.label}
</Text>
</div>
));
return (
<Card padding="sm" withBorder orientation="horizontal">
<Card.Section inheritPadding px="xs" withBorder>
<RingProgress
roundCaps
thickness={6}
size={150}
sections={[{ value: (completed / total) * 100, color: 'blue' }]}
label={
<div>
<Text ta="center" fz="lg">
{((completed / total) * 100).toFixed(0)}%
</Text>
<Text ta="center" fz="xs" c="dimmed">
Completed
</Text>
</div>
}
/>
</Card.Section>
<Card.Section inheritPadding px="md">
<Text fz="xl">Project tasks</Text>
<Box mt="xs">
<Text>1887</Text>
<Text fz="xs" c="dimmed">
Completed
</Text>
</Box>
<Group mt="sm">{items}</Group>
</Card.Section>
</Card>
);
}
Checkbox.Group and Switch.Group now support maxSelectedValues prop to limit the number of selected values. When the limit is reached, the remaining controls are disabled and cannot be selected.
import { Checkbox, Group } from '@mantine/core';
function Demo() {
return (
<Checkbox.Group defaultValue={['react']} maxSelectedValues={2}>
<Group>
<Checkbox value="react" label="React" />
<Checkbox value="svelte" label="Svelte" />
<Checkbox value="ng" label="Angular" />
<Checkbox value="vue" label="Vue" />
</Group>
</Checkbox.Group>
);
}
All Mantine input components based on Input component now support loading prop.
Set loading prop to display a loading indicator. By default, the loader is displayed on the right side of the input. You can change the position with the loadingPosition prop to 'left' or 'right'. This is useful for async operations like API calls, searches, or validations:
import { TextInput } from '@mantine/core';
function Demo() {
return <TextInput placeholder="Your email" loading />;
}
MultiSelect and TagsInput components now support renderPill prop to customize pill rendering:
import { MultiSelect, Pill, Avatar } from '@mantine/core';
const users = [
{ value: 'Emily Johnson', label: 'Emily Johnson', image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-7.png' },
{ value: 'Ava Rodriguez', label: 'Ava Rodriguez', image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-8.png' },
{ value: 'Olivia Chen', label: 'Olivia Chen', image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-4.png' },
{ value: 'Ethan Barnes', label: 'Ethan Barnes', image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-1.png' },
{ value: 'Mason Taylor', label: 'Mason Taylor', image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-2.png' },
];
const usersMap = new Map(users.map((user) => [user.value.toString(), user]));
function Demo() {
return (
<MultiSelect
data={users}
label="Candidates"
placeholder="Select candidates"
defaultValue={['Emily Johnson', 'Ava Rodriguez']}
renderPill={({ option, onRemove }) => {
const user = usersMap.get(option?.value.toString());
return (
<Pill withRemoveButton onRemove={onRemove}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar src={user?.image} size={16} />
{option?.label}
</div>
</Pill>
);
}}
/>
);
}
All clearable input components now support clearSectionMode prop that determines how the clear button and rightSection are rendered:
'both'(default) – render both the clear button andrightSection'rightSection'– render only the user-suppliedrightSection, ignore clear button'clear'– render only the clear button, ignorerightSection
This prop is supported by Select, Autocomplete, MultiSelect, TagsInput, FileInput, DateInput, DatePickerInput, MonthPickerInput, YearPickerInput, TimePicker, and DateTimePicker.
import { CaretDownIcon } from '@phosphor-icons/react';
import { Stack } from '@mantine/core';
import { DatePickerInput } from '@mantine/dates';
function Demo() {
return (
<Stack>
<DatePickerInput
label="clearSectionMode='both' (default)"
placeholder="Pick date"
defaultValue={new Date()}
clearable
rightSection={<CaretDownIcon size={16} />}
clearSectionMode="both"
/>
<DatePickerInput
label="clearSectionMode='rightSection'"
placeholder="Pick date"
defaultValue={new Date()}
clearable
rightSection={<CaretDownIcon size={16} />}
clearSectionMode="rightSection"
/>
<DatePickerInput
label="clearSectionMode='clear'"
placeholder="Pick date"
defaultValue={new Date()}
clearable
rightSection={<CaretDownIcon size={16} />}
clearSectionMode="clear"
/>
</Stack>
);
}
use-form validation rules can now be async – return a Promise that resolves to an error message or null. When all rules are synchronous, form.validate(), form.validateField() and form.isValid() return their results directly (not wrapped in a Promise). When any rule is async, these methods return promises instead. TypeScript infers the correct return type based on your validation rules, so you get precise types without manual annotations.
The form.validating property is true while any async validation is in progress, and form.isValidating(path) checks individual fields. The validating state is never set for forms with only synchronous rules.
Each rule receives an AbortSignal as the fourth argument. The signal is aborted when a newer validation supersedes the current one, which you can use to cancel in-flight HTTP requests.
import { Button, Group, Loader, TextInput } from '@mantine/core';
import { isEmail, useForm } from '@mantine/form';
// Simulates an async API call to check if the username is available
function checkUsernameAvailability(username: string, signal?: AbortSignal): Promise<string | null> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const taken = ['admin', 'user', 'test', 'mantine'];
resolve(taken.includes(username.toLowerCase()) ? 'Username is already taken' : null);
}, 800);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { username: '', email: '' },
validate: {
username: async (value, _values, _path, signal) => {
if (value.trim().length < 3) {
return 'Username must be at least 3 characters';
}
return checkUsernameAvailability(value, signal);
},
email: isEmail('Invalid email'),
},
});
return (
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput
withAsterisk
label="Username"
placeholder="Pick a username"
key={form.key('username')}
disabled={form.submitting}
rightSection={form.validating ? <Loader size={16} /> : null}
{...form.getInputProps('username')}
/>
<TextInput
withAsterisk
mt="md"
label="Email"
placeholder="your@email.com"
key={form.key('email')}
disabled={form.submitting}
{...form.getInputProps('email')}
/>
<Group justify="flex-end" mt="md">
<Button type="submit" loading={form.submitting}>
Submit
</Button>
</Group>
</form>
);
}
When using async validation with validateInputOnChange, set validateDebounce to avoid firing an API call on every keystroke:
import { Button, Group, Loader, TextInput } from '@mantine/core';
import { useForm, isEmail } from '@mantine/form';
// Simulates an async API call to check if the username is available
function checkUsernameAvailability(username: string, signal?: AbortSignal): Promise<string | null> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const taken = ['admin', 'user', 'test', 'mantine'];
resolve(taken.includes(username.toLowerCase()) ? 'Username is already taken' : null);
}, 800);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { username: '', email: '' },
// Debounce async validation by 500ms – prevents firing
// an API call on every keystroke
validateDebounce: 500,
validateInputOnChange: ['username'],
validate: {
username: async (value, _values, _path, signal) => {
if (value.trim().length < 3) {
return 'Username must be at least 3 characters';
}
return checkUsernameAvailability(value, signal);
},
email: isEmail('Invalid email'),
},
});
return (
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<TextInput
withAsterisk
label="Username"
description="Try: admin, user, test, mantine"
placeholder="Pick a username"
key={form.key('username')}
disabled={form.submitting}
rightSection={form.isValidating('username') ? <Loader size={16} /> : null}
{...form.getInputProps('username')}
/>
<TextInput
withAsterisk
mt="md"
label="Email"
placeholder="your@email.com"
key={form.key('email')}
disabled={form.submitting}
{...form.getInputProps('email')}
/>
<Group justify="flex-end" mt="md">
<Button type="submit" loading={form.submitting}>
Submit
</Button>
</Group>
</form>
);
}
use-form now supports passing second type argument TransformedValues to define the type of transformed values returned by form.getTransformedValues and form.onSubmit:
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),
}),
});
}
SegmentedControl, Select, MultiSelect, Chip.Group, Switch.Group, Checkbox.Group and Radio.Group now support generic value type. You can pass primitive values (numbers, strings, booleans, bigint) as the type argument. The generic type is used for value, defaultValue, onChange and other props.
For example, generic type can now be used with SegmentedControl to specify string union:
import { SegmentedControl } from '@mantine/core';
function Demo() {
return (
<SegmentedControl<'orange' | 'grape' | 'apple'>
data={[
{ value: 'orange', label: 'Orange' },
{ value: 'grape', label: 'Grape' },
{ value: 'apple', label: 'Apple' },
]}
/>
);
}
Combobox component now supports virtualization with the useVirtualizedCombobox hook. The hook does not depend on any specific virtualization library. The recommended option is @tanstack/react-virtual.
Example of implementation with useVirtualizedCombobox and @tanstack/react-virtual:
import { useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Combobox, Input, InputBase, ScrollArea, useVirtualizedCombobox } from '@mantine/core';
const ITEM_HEIGHT = 36;
const largeData = Array(10000)
.fill(0)
.map((_, index) => ({
value: `value-${index}`,
label: `Label ${index}`,
id: `item-${index}`,
disabled: false,
}));
function Demo() {
const [opened, setOpened] = useState(false);
const [selectedOptionIndex, setSelectedOptionIndex] = useState(-1);
const [activeOptionIndex, setActiveOptionIndex] = useState(-1);
const [value, setValue] = useState('');
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
const virtualizer = useVirtualizer({
count: largeData.length,
getScrollElement: () => scrollParent,
estimateSize: () => ITEM_HEIGHT,
overscan: 5,
});
const combobox = useVirtualizedCombobox({
opened,
onOpenedChange: setOpened,
onDropdownOpen: () => {
if (activeOptionIndex !== -1) {
setSelectedOptionIndex(activeOptionIndex);
requestAnimationFrame(() => {
virtualizer.scrollToIndex(activeOptionIndex, { align: 'auto' });
});
}
},
isOptionDisabled: (index) => largeData[index].disabled,
totalOptionsCount: largeData.length,
getOptionId: (index) => largeData[index].id,
selectedOptionIndex,
activeOptionIndex,
setSelectedOptionIndex: (index) => {
setSelectedOptionIndex(index);
if (index !== -1) {
virtualizer.scrollToIndex(index, { align: 'auto' });
}
},
onSelectedOptionSubmit: onOptionSubmit,
});
function onOptionSubmit(index: number) {
const option = largeData[index];
setValue(option.value);
setActiveOptionIndex(index);
combobox.closeDropdown();
combobox.resetSelectedOption();
}
return (
<Combobox store={combobox} resetSelectionOnOptionHover={false} keepMounted>
<Combobox.Target>
<InputBase component="button" onClick={() => combobox.toggleDropdown()} pointer>
{value || <Input.Placeholder>Pick a value</Input.Placeholder>}
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
<ScrollArea.Autosize
mah={220}
type="scroll"
scrollbarSize={4}
viewportRef={setScrollParent}
onMouseDown={(event) => event.preventDefault()}
>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = largeData[virtualItem.index];
return (
<Combobox.Option
value={item.value}
key={item.value}
active={virtualItem.index === activeOptionIndex}
selected={virtualItem.index === selectedOptionIndex}
onClick={() => onOptionSubmit(virtualItem.index)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{item.label}
</Combobox.Option>
);
})}
</div>
</ScrollArea.Autosize>
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
}
You can find more virtualization examples on the Combobox examples page.
Highlight component now supports custom colors for individual highlight terms. You can provide an array of objects with text and color properties to assign different colors to different highlighted terms:
import { Highlight } from '@mantine/core';
function Demo() {
return (
<Highlight
highlight={[
{ text: 'error', color: 'red' },
{ text: 'warning', color: 'yellow' },
{ text: 'success', color: 'green' },
]}
>
Error: Invalid input. Warning: Check this field. Success: All tests passed.
</Highlight>
);
}
Highlight component now supports wholeWord prop to match only complete words. When enabled, 'the' will not match 'there' or 'theme':
import { Highlight, Stack, Text } from '@mantine/core';
function Demo() {
return (
<Stack gap="md">
<div>
<Text size="sm" fw={500} mb={5}>
With whole word matching (wholeWord={'{'}true{'}'})
</Text>
<Highlight highlight="the" wholeWord>
The theme is there
</Highlight>
</div>
<div>
<Text size="sm" fw={500} mb={5}>
Without whole word matching (default)
</Text>
<Highlight highlight="the">The theme is there</Highlight>
</div>
</Stack>
);
}
Pagination component and use-pagination hook now support startValue prop to define the starting page number. For example, with startValue={5} and total={15}, the pagination range will be from 5 to 15:
import { Text, Pagination } from '@mantine/core';
function Demo() {
return (
<>
<Text mb="xs">Pages 5–15 (startValue=5, total=15)</Text>
<Pagination total={15} startValue={5} defaultValue={5} />
</>
);
}
Grid component no longer uses negative margins for spacing between columns. Instead, it now uses native CSS gap property, which means you no longer need to use overflow="hidden" to prevent content overflow caused by negative margins.
Slider and RangeSlider components now support vertical orientation:
import { RangeSlider, Slider } from '@mantine/core';
const marks = [
{ value: 20, label: '20%' },
{ value: 50, label: '50%' },
{ value: 80, label: '80%' },
];
function Demo() {
return (
<div style={{ display: 'flex', gap: 40 }}>
<Slider orientation="vertical" defaultValue={45} marks={marks} />
<RangeSlider orientation="vertical" defaultValue={[25, 65]} marks={marks} />
</div>
);
}
SimpleGrid component now supports minColWidth prop to use CSS Grid auto-fill/auto-fit to automatically adjust the number of columns based on available space and minimum column width. When minColWidth is set, the cols prop is ignored. Use autoFlow prop to switch between auto-fill (default) and auto-fit behavior.
import { SimpleGrid } from '@mantine/core';
function Demo() {
return (
<SimpleGrid minColWidth="200px">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</SimpleGrid>
);
}
SimpleGrid also now supports autoRows prop to control the size of implicitly created grid rows:
import { SimpleGrid } from '@mantine/core';
function Demo() {
return (
<SimpleGrid cols={3} autoRows="minmax(100px, auto)">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</SimpleGrid>
);
}
Calendar, DatePicker, MonthPicker and YearPicker components now support fullWidth prop to make the calendar stretch to fill 100% of its parent container width:
import { Calendar } from '@mantine/dates';
function Demo() {
return <Calendar fullWidth />;
}
Many Mantine components and hooks now provide namespace exports for related types. For example, use-disclosure hook types can now be accessed like this:
import { useDisclosure } from '@mantine/hooks';
const options: useDisclosure.Options = {
onOpen: () => console.log('open'),
onClose: () => console.log('close'),
};
function Demo() {
const [opened, handlers] = useDisclosure(options);
}
Example of using namespace types with Button props type:
import { Button } from '@mantine/core';
const buttonProps: Button.Props = {
variant: 'filled',
size: 'md',
disabled: false,
};
function Demo() {
return <Button {...buttonProps}>Click me</Button>;
}
New fontWeights property was added to the theme object. It allows you to control font-weight values used across all components. The default values are:
regular–400medium–600bold–700
Each value is mapped to a CSS variable: --mantine-font-weight-regular, --mantine-font-weight-medium, --mantine-font-weight-bold. All components that previously used hardcoded font-weight values now use these CSS variables.
For example, to revert the medium font weight from 600 back to 500 (the default in Mantine 8):
import { createTheme, MantineProvider } from '@mantine/core';
const theme = createTheme({
fontWeights: {
medium: '500',
},
});
function Demo() {
return (
<MantineProvider theme={theme}>
{/* Your app here */}
</MantineProvider>
);
}
Mantine now includes @mantine/mcp-server package that exposes Mantine documentation over Model Context Protocol. It allows AI tools to query Mantine docs and props data through MCP tools instead of raw web scraping.
The server uses static data generated from Mantine documentation and serves:
- item discovery with
list_items - full documentation page retrieval with
get_item_doc - normalized props metadata with
get_item_props - keyword-based lookup with
search_docs
This setup is useful in agent workflows where tools can call MCP functions directly to retrieve structured data and reduce prompt size.
Basic server configuration:
{
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server"],
"env": {
"MANTINE_MCP_DATA_URL": "https://mantine.dev/mcp"
}
}
}
}
For setup details, supported tools, and client-specific instructions, see Mantine with LLMs.
Mantine skills for AI coding agents are documented in the Mantine with LLMs guide.
The guide includes:
- available skills in
mantinedev/skills - installation commands for each skill
- separate usage instructions for Claude Code and Codex
New isUrl and isOneOf validators were added to @mantine/form package:
isUrl– validates that the value is a valid URL. Supports custom protocols and localhost option.isOneOf– validates that the value is included in the given list of allowed values.
import { isOneOf, isUrl, useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { website: '', role: '' },
validate: {
website: isUrl('Invalid URL'),
role: isOneOf(['admin', 'user'], 'Invalid role'),
},
});
@mantine/form now has built-in support for Standard Schema, a community specification implemented by Zod v4, Valibot, ArkType, and other schema libraries. Use schemaResolver to validate forms with any compliant library without installing a separate resolver package:
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const schema = z.object({
name: z.string().min(2, { error: 'Name should have at least 2 letters' }),
email: z.email({ error: 'Invalid email' }),
age: z.number().min(18, { error: 'You must be at least 18 to create an account' }),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '', age: 16 },
validate: schemaResolver(schema, { sync: true }),
});
Pass { sync: true } when your schema is synchronous to get synchronous return types for form.validate(), form.validateField(), and form.isValid().
AppShell component now supports mode="static" which renders all AppShell elements as part of the normal document flow using CSS Grid instead of fixed positioning. Static mode supports layout="alt" to place navbar and aside at full height with header and footer adjusted between them. See AppShell examples for more details.
ScrollArea component now supports startScrollPosition prop to set the initial scroll position when the component mounts:
import { ScrollArea } from '@mantine/core';
function Demo() {
return (
<ScrollArea h={200} startScrollPosition={{ y: 250 }}>
{/* ... content */}
</ScrollArea>
);
}
Five hooks — usePageLeave, useWindowEvent, useHotkeys, useClickOutside, and useCollapse — have been updated to use React 19's stable useEffectEvent. Previously these hooks captured stale closures or caused unnecessary event listener re-registrations when non-memoized callbacks were passed. With useEffectEvent, each hook registers a single stable listener that always calls the latest version of the provided callback, so wrapping callbacks in useCallback or useMemo is no longer required.
Transition, Collapse, Tabs.Panel, Stepper, and Tree now use React 19's Activity component when keepMounted is set. Activity preserves the state of hidden subtrees — form inputs, scroll positions, and any other component state survive while the content is not visible. Stepper and Tree gain a new keepMounted prop; all other components already had it.
import { useState } from 'react';
import { Stepper, TextInput } from '@mantine/core';
function Demo() {
const [active, setActive] = useState(0);
// TextInput values are preserved when navigating between steps
return (
<Stepper active={active} onStepClick={setActive} keepMounted>
<Stepper.Step label="Step 1">
<TextInput label="Value survives navigation" />
</Stepper.Step>
<Stepper.Step label="Step 2">
<TextInput label="Value survives navigation" />
</Stepper.Step>
</Stepper>
);
}
- New Custom components guide explaining how to create custom components with Mantine's styling system
- New Controlled vs Uncontrolled guide explaining differences between controlled and uncontrolled components
- HueSlider and AlphaSlider components now have their own documentation pages
- Uncontrolled documentation and usage with
FormDatasection was added to all inputs components - JsonInput documentation now includes custom serialization example with
superjsonlibrary - Pagination documentation now includes URL synchronization examples for Next.js, react-router-dom and nuqs
- use-form documentation now includes separate examples with all Mantine inputs
- Notifications component now pauses auto close timer of all visible notifications when any notification is hovered (new default). Use
pauseResetOnHover="notification"to keep the previous behavior of pausing only the hovered notification. - useHeadroom hook now returns
{ pinned: boolean; scrollProgress: number }object instead of a plainboolean.scrollProgressis a value between0(fully hidden) and1(fully visible) that can be used for scroll-linked reveal animations. A newscrollDistanceoption controls how many pixels of scrolling are required to fully reveal or hide the element (default:100). - New useScrollDirection hook detects whether the user is currently scrolling up or down. It returns
'up','down', or'unknown', handles resize events to avoid false direction changes, and always uses the latest state without stale closure issues. - Default
theme.defaultRadiuswas changed fromsm(4px) tomd(8px) lightvariant in all components now uses different colors values without transparency to improve contrastmodprop now converts camelCase keys to kebab-case for data attributes in all components@mantine/formpackage now includes built-in Standard Schema support viaschemaResolver@mantine/formgetInputPropsnow supportstype: 'radio'for individual radio inputs – returnschecked/defaultCheckedand passes through the radio optionvalue@mantine/formnow supports async validation rules.form.validate(),form.validateField()andform.isValid()return results directly when all rules are synchronous and return promises only when async rules are present. Newform.validating,form.isValidating(path),validateDebounceandresolveValidationErroroptions were added.createPolymorphicComponentfunction was renamed to shorterpolymorphicfor convenience- Mantine components now use theme-controlled
fontWeightsvalues. The defaultmediumfont weight was changed from500to600for better readability. - All Mantine components now support logical margin and padding style props:
mis- margin-inline-startmie- margin-inline-endpis- padding-inline-startpie- padding-inline-end
- Tree component now supports controlled state via
expandedState,selectedStateandcheckedStateprops. - Tree component no longer defines
data-hoveredattribute for hover state, you need to apply hover styles with&:hoverinstead. This change improves rendering performance by resolving this issue. - Collapse component now uses
expandedprop instead ofin - Collapse, NavLink and Accordion.Panel now support
keepMounted={false}prop to unmount collapsed content - Select and MultiSelect components now support primitive value types (numbers, booleans, strings) for data and value
- MultiSelect now supports
onMaxValuesprop, which is called when the user attempts to select more values thanmaxValues - TagsInput component now supports
onMaxTagsprop, which is called when the user attempts to add more tags thanmaxTags - Accordion component now supports
refprop - Text and Anchor components no longer accept
colorprop, usecstyle prop instead - PasswordInput component visibility toggle icon was updated
- Popover and Tooltip components no longer accept
positionDependenciesprop, it is no longer required - TypographyStylesProvider component was renamed to Typography
- Checkbox component now supports
readOnlyandwithErrorStylesprops - Spoiler component:
initialStateprop was renamed todefaultExpandedfor consistency with other components- New
showAriaLabelandhideAriaLabelprops allow customizing ARIA labels
- Checkbox.Group and Switch.Group can now be used in uncontrolled forms and can be accessed through
FormData - ColorPicker component now supports
nameandhiddenInputPropsprops to include color value in uncontrolled form submissions - Dialog now enables
withBorderby default - Pagination component now supports
input-prefix forsizeprop to match input and button sizes - FloatingIndicator component now supports
onTransitionStartandonTransitionEndcallbacks - LoadingOverlay component now supports
onEnter,onEntered,onExitandonExitedcallbacks - Grid component
gutterprop was renamed togapfor consistency with other layout components. NewrowGapandcolumnGapprops allow separate control of row and column spacing. Grid.Col now supportsalignprop for per-column vertical alignment. - Indicator component now supports:
maxValueprop to display{maxValue}+when the label exceeds the maximum valueshowZeroprop (defaulttrue) to control visibility of indicator with label0offsetprop object withxandyproperties for separate horizontal and vertical offsets
- NumberInput component now supports:
onMinReachedandonMaxReachedcallbacksselectAllOnFocusprop to select all text when input is focusedbigintvalues forvalue,defaultValue,onChange,min,max,stepandstartValue(withstringfallback for intermediate states)
- RingProgress component now supports
sectionGapprop to add visual separation between sections in degreesstartAngleprop to control where the progress starts (0 = right, 90 = bottom, 180 = left, 270 = top)
- List component now supports HTML5 list attributes:
start,reversed, andvalueprops for enhanced semantic HTML support - JsonInput component now supports
indentSpacesprop to control the number of spaces used for formatting JSON - Rating component now supports
allowClearprop to reset rating to 0 by clicking the same value - ScrollArea component now supports
onLeftReachedandonRightReachedcallbacks for horizontal scroll boundaries - Slider and RangeSlider now support hidden marks with
hidden: trueproperty. Hidden marks allow snapping to specific values without displaying them visually, useful withrestrictToMarksprop. - use-tree no longer supports callback state setters for
setExpandedState,setSelectedState, andsetCheckedStatefunctions - use-fullscreen hook was split into two hooks:
useFullscreenElementanduseFullscreenDocument - use-media-query hook no longer includes fallback for old Safari versions (iOS 13 and earlier, released before 2019)
- use-resize-observer now uses the new callback ref approach. The new approach makes hook usable with dynamic node changes. This change might be breaking, validate hook usage in your application.
- use-mouse hook now uses the new callback ref approach to resolve the issue with stale refs. The previous hook functionality was split into two hooks:
useMouse(for ref) anduseMousePosition(for document). - use-mutation-observer hook now uses the new callback ref approach. The new approach makes hook usable with dynamic node changes. Additionally, a new
useMutationObserverTargethook was added for observing external target elements. - use-disclosure hook now supports new
sethandler - use-floating-indicator hook now supports
onTransitionStartandonTransitionEndcallbacks @mantine/hookstypes were renamed for consistency:UseScrollSpyReturnType→UseScrollSpyReturnValueStateHistory→UseStateHistoryValueOS→UseOSReturnValue
8.3.18
This is the last 8.x release. You are welcome to test 9.0 alpha version and provide feedback before its release on March 31 – https://alpha.mantine.dev/changelog/9-0-0/
[@mantine/core]PasswordInput: Fix styles api props not resolving correctly in theme (#8716)
8.3.17
This is the last 8.x release. You are welcome to test 9.0 alpha version and provide feedback before its release on March 31.
https://alpha.mantine.dev/changelog/9-0-0/
[@mantine/core]Stepper: Fix Google Translate compatibility issues (#8744)[@mantine/hooks]use-list-state: Add memoization to all handlers (#8739)