9.2.0

mantinedev/mantine9.2.0May 11, 2026by rtivital

AI Summary

A feature release introducing TreeSelect, notification swipe dismissal, new hooks, and chart components.

Key Highlights

  • New TreeSelect component for hierarchical data
  • Notifications now support swipe dismissal
  • New `use-drag` hook for pointer gestures
  • New InlineDateTimePicker and SankeyChart components
  • New RollingNumber component for value animations

New Features

  • TreeSelect component
  • Notifications swipe dismissal
  • use-drag hook
  • InlineDateTimePicker component
  • DateTimePicker range support
  • RollingNumber component
  • MaskInput resetRef prop
  • SankeyChart component
  • Pill reordering in MultiSelect/TagsInput
  • Tree drag handle and restricted drop targets
  • Shared default props for inputs
  • Per-day business hours in WeekView

Full Release Notes

[View changelog with demos on mantine.dev website](https://mantine.dev/changelog/9-2-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.

## TreeSelect component

New [TreeSelect](https://mantine.dev/core/tree-select) component allows picking one or more values from hierarchical tree data.
It supports three selection modes: single, multiple, and checkbox (with parent-child cascade):

```tsx
import { TreeSelect } from '@mantine/core';
import { data } from './data';

function Demo() {
  return (
    <TreeSelect
      label="Your favorite item"
      placeholder="Pick value"
      data={data}
    />
  );
}
```

## Tree select Combobox examples

New [Combobox examples](https://mantine.dev/combobox?e=TreeSelectCombobox) showing how to build tree select components
from Combobox primitives with connecting lines, expand/collapse chevrons, and proper indentation:

- [Tree select](https://mantine.dev/combobox?e=TreeSelectCombobox) – basic single-value tree select
- [Tree multi select](https://mantine.dev/combobox?e=TreeMultiSelectCombobox) – multi select with checkbox cascade
- [Searchable tree select](https://mantine.dev/combobox?e=TreeSelectSearchable) – tree select with search filtering
- [Tree select with checkboxes](https://mantine.dev/combobox?e=TreeSelectCheckbox) – single select with expand-on-click
- [Virtualized tree select](https://mantine.dev/combobox?e=TreeSelectVirtualized) – large tree (~500 nodes) with @tanstack/react-virtual

## Notifications swipe dismissal

[@mantine/notifications](https://mantine.dev/x/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`.

```tsx
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>
  );
}
```

## use-drag hook

New [use-drag](https://mantine.dev/hooks/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:

```tsx
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>
  );
}
```

## InlineDateTimePicker component

New [InlineDateTimePicker](https://mantine.dev/dates/inline-date-time-picker) component renders a calendar
with a time picker inline, without a dropdown. It supports both default and range modes:

```tsx
import { InlineDateTimePicker } from '@mantine/dates';

function Demo() {
  return <InlineDateTimePicker />;
}
```

Set `type="range"` to select a date and time range with two time inputs:

```tsx
import { InlineDateTimePicker } from '@mantine/dates';

function Demo() {
  return <InlineDateTimePicker type="range" />;
}
```

## DateTimePicker range support

[DateTimePicker](https://mantine.dev/dates/date-time-picker) 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:

```tsx
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 function

[DateTimePicker](https://mantine.dev/dates/date-time-picker) `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:

```tsx
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"
    />
  );
}
```

## RollingNumber component

New [RollingNumber](https://mantine.dev/core/rolling-number) component animates value changes with rolling digit
transitions. Each digit independently rolls to its new position when the value changes:

```tsx
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 improvements

[MaskInput](https://mantine.dev/core/mask-input) 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:

```tsx
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](https://mantine.dev/form/use-form) is now documented. Use `defaultValue`
to seed the initial value and `onChangeRaw` to write the raw value to form state:

```tsx
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>
  );
}
```

## SankeyChart component

New [SankeyChart](https://mantine.dev/charts/sankey-chart) component visualizes flow between nodes as a Sankey diagram
where the width of each link is proportional to the flow value:

```tsx
// 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 },
  ],
};
```

## Reorder pills in MultiSelect and TagsInput

[MultiSelect](https://mantine.dev/core/multi-select) and [TagsInput](https://mantine.dev/core/tags-input) 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 `Tab` order. `ArrowLeft` from the input (caret at start) moves
  focus to the last pill.
- `ArrowLeft` and `ArrowRight` navigate between pills (RTL-aware). `ArrowRight` on the last
  pill returns focus to the input.
- `Alt + ArrowLeft` and `Alt + ArrowRight` reorder 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:

```tsx
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
    />
  );
}
```

## Restrict Tree drop targets

[Tree](https://mantine.dev/core/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:

```tsx
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 drag handle

[Tree](https://mantine.dev/core/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:

```tsx
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} />}
    />
  );
}
```

## Shared default props for all inputs

Default props set on `Input` and `Input.Wrapper` in `theme.components` now cascade to every
component built on top of them ([TextInput](https://mantine.dev/core/text-input/), [Textarea](https://mantine.dev/core/textarea/),
[NumberInput](https://mantine.dev/core/number-input/), [Select](https://mantine.dev/core/select/), [DateInput](https://mantine.dev/dates/date-input/),
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:

```tsx
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>
  );
}
```

## Per-day business hours in WeekView

[WeekView](https://mantine.dev/schedule/week-view) `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:

```tsx
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"
    />
  );
}
```