mantinedev/mantine
 Watch   
 Star   
 Fork   
3 hours ago
mantine

9.7.0

View changelog with demos on mantine.dev website

Support Mantine development

You can now sponsor Mantine development with OpenCollective. All funds are used to improve Mantine and create new features and components.

@mantine/mcp-server documentation coverage

@mantine/mcp-server now exposes 406 documentation pages instead of 164. Previously only components were indexed – hooks, @mantine/form, styling, theming, guides and help.mantine.dev FAQ pages are now included as well. Changelog pages are deliberately excluded to keep search results focused on current APIs.

Documentation is now retrieved by section rather than by whole page. get_item_doc returns a page outline (description, import statement and the list of section headings) by default, a single section with the section argument, or the entire page with full: true. Requesting useForm this way returns about 1KB instead of 9KB, and the largest pages drop from roughly 89KB to 4KB.

Other improvements:

  • New get_api tool – resolves a symbol such as useForm or useDisclosure to its TypeScript signature and exported types without knowing which page documents it
  • Ranked search – search_docs results point at the specific section that matched, so a query like "validate a form with zod" resolves to the zod section of the schema validation page
  • Search synonyms – queries now match the search keywords maintained in the documentation, so "command palette" finds Spotlight
  • More filters – list_items accepts group, category and package in addition to kind
  • Hook signatures – get_item_props returns the TypeScript signature for hooks, and points at the relevant section for pages that document their API in prose

The server was also migrated to the official Model Context Protocol SDK, so the protocol version is negotiated with the client instead of being pinned.

For setup details, supported tools, and client-specific instructions, see Mantine with LLMs.

Tour component

New Tour component provides interactive step-by-step application onboarding. It highlights target elements with a spotlight overlay and displays tooltips with navigation controls. Supports both guided (sequential) and beacon (non-sequential) modes.

Toolbar component

New Toolbar component provides an accessible toolbar container with roving tabindex keyboard navigation, toggle groups with single/multiple selection, visual grouping, and dividers. Styling is similar to RichTextEditor toolbar.

Toggle component

New Toggle component is a button that can be toggled between active and inactive states:

JsonViewer component

New JsonViewer component can be used to display complex JSON structures with expand/collapse, type indicators, copy to clipboard, and other features:

Light Checkbox and Radio variant

Checkbox and Radio components now support variant="light" – the indicator uses a light background color with a colored icon, similar to the light variant available in other components like Button:

import { Radio, Checkbox, Stack } from '@mantine/core';

function Demo() {
  return (
    <Stack gap={7}>
      <Checkbox variant="light" label="Light Checkbox" defaultChecked />
      <Checkbox variant="light" label="Light indeterminate Checkbox" indeterminate />
      <Radio variant="light" label="Light Radio" defaultChecked />
    </Stack>
  );
}

RichTextEditor image upload

RichTextEditor now supports image upload with loading states. Images can be uploaded via toolbar button, drag and drop, or paste. During upload, a local preview is shown with a loading indicator.

Configuring getUploadImageExtension is enough to enable uploads – onImageUpload and onImageUploadError props of RichTextEditor are optional overrides for the toolbar control. onImageUpload can resolve either to a string or to { src, alt, title } (new exported ImageUploadResult type) to set alternative text on the uploaded image. Several files picked, dropped or pasted together are inserted in the order they were selected and are undone as one step.

import TipTapImage from '@tiptap/extension-image';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { getUploadImageExtension, ImageUploadResult, RichTextEditor } from '@mantine/tiptap';

function handleImageUpload(file: File): Promise<ImageUploadResult> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ src: URL.createObjectURL(file), alt: file.name });
    }, 3000);
  });
}

function Demo() {
  const editor = useEditor({
    shouldRerenderOnTransaction: true,
    extensions: [
      StarterKit,
      getUploadImageExtension(TipTapImage, { onImageUpload: handleImageUpload }),
    ],
    content: '<p>Click the image button in the toolbar, or drag & drop / paste an image into the editor.</p>',
  });

  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Bold />
          <RichTextEditor.Italic />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.ImageUpload />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor>
  );
}

Schedule event overlap mode

DayView and WeekView now support eventOverlapMode prop that controls how events overlapping in time share the horizontal space. The default columns mode splits the available width evenly between them, which becomes unreadable once a large number of events overlap. The new cascade mode indents each event and stacks it over the previous one, so every event keeps a readable width no matter how many of them overlap.

The indent shrinks automatically as the group grows, so the event on top always keeps at least 60% of the available width. Covered events can still be read and used: rest the pointer on one and it is raised above the events covering it, which also makes its resize handles reachable. Keyboard users get the same result by focusing the event with Tab. eventOverlapRaiseDelay prop controls how long the pointer must rest on an event before it is raised, in milliseconds (600 by default).

import { useState } from 'react';
import { SegmentedControl, Stack } from '@mantine/core';
import { ScheduleEventOverlapMode, WeekView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  const [mode, setMode] = useState<ScheduleEventOverlapMode>('cascade');

  return (
    <Stack>
      <SegmentedControl
        value={mode}
        onChange={(value) => setMode(value as ScheduleEventOverlapMode)}
        data={[
          { value: 'columns', label: 'columns' },
          { value: 'cascade', label: 'cascade' },
        ]}
      />

      <WeekView
        date={new Date()}
        events={events}
        startTime="08:00:00"
        endTime="14:00:00"
        eventOverlapMode={mode}
      />
    </Stack>
  );
}

Schedule drop validation

All Schedule views can now reject a drag or resize before it is committed, based on where the event is being dropped rather than on which event is being dragged. Previously canDragEvent and canResizeEvent only decided whether an event was draggable or resizable at all, so the only way to prevent a double booking was to accept the drop, notice the conflict in the handler and not update state – which produced a snap-back with no explanation:

  • preventEventOverlap rejects placements that would make the event overlap another event. Pass a function to decide per pair of events: return true to forbid that particular overlap.
  • canDropEvent and canResizeEventTo are called before a drop or resize is committed – return false to reject it.
  • canDropExternalEvent does the same for items dragged in from outside the schedule.
  • onEventPlacementRejected reports the rejection with the conflicting events and the reason.

The checks compose: a placement is allowed only when the overlap check passes and the matching callback returns a value other than false. The overlap check runs first, so onEventPlacementRejected can report conflicts without your code repeating the work. While the pointer is over an invalid target the drag ghost and the resizing event get data-invalid, so the rejection is visible before the pointer is released.

import { useState } from 'react';
import dayjs from 'dayjs';
import { Stack, Text } from '@mantine/core';
import { WeekView, ScheduleEventData } from '@mantine/schedule';

const startOfWeek = dayjs()
  .subtract((dayjs().day() + 6) % 7, 'day')
  .format('YYYY-MM-DD');

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Standup',
    start: `${startOfWeek} 09:00:00`,
    end: `${startOfWeek} 09:30:00`,
    color: 'blue',
  },
  {
    id: 2,
    title: 'Design review',
    start: `${startOfWeek} 11:00:00`,
    end: `${startOfWeek} 12:00:00`,
    color: 'grape',
  },
  {
    id: 3,
    title: 'Retro',
    start: `${startOfWeek} 14:00:00`,
    end: `${startOfWeek} 15:00:00`,
    color: 'teal',
  },
];

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);
  const [message, setMessage] = useState<string | null>(null);

  return (
    <Stack>
      <WeekView
        date={date}
        onDateChange={setDate}
        events={events}
        startTime="08:00:00"
        endTime="18:00:00"
        withEventsDragAndDrop
        withEventResize
        eventDragInterval={15}
        preventEventOverlap
        onEventDrop={({ eventId, newStart, newEnd }) => {
          setMessage(null);
          setEvents((prev) =>
            prev.map((event) =>
              event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
            )
          );
        }}
        onEventResize={({ eventId, newStart, newEnd }) => {
          setMessage(null);
          setEvents((prev) =>
            prev.map((event) =>
              event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
            )
          );
        }}
        onEventPlacementRejected={(data) => {
          if (data.action === 'external-drop') {
            return;
          }

          setMessage(
            `${data.event.title} cannot be ${data.action === 'drop' ? 'moved' : 'resized'} there – it would overlap ${data.conflicts
              .map((conflict) => conflict.title)
              .join(', ')}`
          );
        }}
      />

      {message && <Text c="red" size="sm">{message}</Text>}
    </Stack>
  );
}

In resource views the conflict set is scoped to the target resource, so moving an event onto another resource row is checked against that row rather than the one it came from:

import { useState } from 'react';
import dayjs from 'dayjs';
import { Stack, Text } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');

const resources: ScheduleResourceData[] = [
  { id: 'room-a', label: 'Room A' },
  { id: 'room-b', label: 'Room B' },
  { id: 'room-c', label: 'Room C (large groups only)' },
];

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Interview (2 people)',
    start: `${today} 09:00:00`,
    end: `${today} 10:00:00`,
    color: 'blue',
    resourceId: 'room-a',
    payload: { attendees: 2 },
  },
  {
    id: 2,
    title: 'All-hands (40 people)',
    start: `${today} 11:00:00`,
    end: `${today} 12:00:00`,
    color: 'grape',
    resourceId: 'room-c',
    payload: { attendees: 40 },
  },
];

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);
  const [message, setMessage] = useState<string | null>(null);

  return (
    <Stack>
      <ResourcesDayView
        date={date}
        onDateChange={setDate}
        resources={resources}
        events={events}
        startTime="08:00:00"
        endTime="18:00:00"
        withEventsDragAndDrop
        preventEventOverlap
        canDropEvent={({ event, resourceId }) =>
          resourceId !== 'room-c' || (event.payload?.attendees ?? 0) >= 20
        }
        onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
          setMessage(null);
          setEvents((prev) =>
            prev.map((event) =>
              event.id === eventId
                ? { ...event, start: newStart, end: newEnd, resourceId }
                : event
            )
          );
        }}
        onEventPlacementRejected={(data) => {
          if (data.action === 'external-drop') {
            return;
          }

          setMessage(
            data.reason === 'overlap'
              ? `${data.event.title} would overlap ${data.conflicts.length} event(s) in that room`
              : 'Room C is reserved for meetings with 20 or more attendees'
          );
        }}
      />

      {message && <Text c="red" size="sm">{message}</Text>}
    </Stack>
  );
}

TreeSelect renderPill

TreeSelect now supports renderPill prop to customize how pills are rendered for selected values in multiple and checkbox modes, similar to renderPill prop of MultiSelect. The callback receives the tree node, so pills can use nodeProps or the node structure, for example to show a different icon for parent and leaf nodes:

import { FileTextIcon, FolderSimpleIcon } from '@phosphor-icons/react';
import { Group, Pill, TreeSelect, TreeSelectProps } from '@mantine/core';
import { data } from './data';

const renderTreePill: TreeSelectProps['renderPill'] = ({ node, onRemove, disabled, readOnly }) => (
  <Pill withRemoveButton={!readOnly} onRemove={onRemove} disabled={disabled}>
    <Group gap={4} wrap="nowrap">
      {node.children || node.hasChildren ? (
        <FolderSimpleIcon size={12} />
      ) : (
        <FileTextIcon size={12} />
      )}
      {node.label}
    </Group>
  </Pill>
);

function Demo() {
  return (
    <TreeSelect
      mode="checkbox"
      checkedStrategy="parent"
      label="Categories"
      placeholder="Pick categories"
      data={data}
      defaultValue={['phones', 'headphones']}
      defaultExpandAll
      renderPill={renderTreePill}
    />
  );
}

Tooltip hideDetached

Tooltip now supports hideDetached prop, which works the same way as Popover hideDetached. It is enabled by default: the tooltip is now hidden when its target element is removed from the DOM, hidden with display: none, or scrolled out of a container that clips it. Previously, the tooltip stayed in place, floating over unrelated content. Set hideDetached={false} to restore the previous behavior. The prop has no effect on Tooltip.Floating, which is positioned relative to the mouse pointer.

Note that jsdom reports zero-size rectangles for every element, so in Jest and Vitest every tooltip is treated as detached and hidden unless the test wrapper sets env="test" on MantineProvider (as described in the Jest and Vitest guides) or the component is rendered with hideDetached={false}.

import { Box, Button, Group, Tooltip } from '@mantine/core';

function Demo() {
  return (
    <Box
      bd="1px solid var(--mantine-color-dimmed)"
      p="xl"
      w={{ base: 340, sm: 400 }}
      h={200}
      style={{ overflow: 'auto' }}
    >
      <Box w={1000} h={400}>
        <Group>
          <Tooltip label="Hidden when detached" position="bottom" opened>
            <Button>Hides when detached</Button>
          </Tooltip>

          <Tooltip label="Visible when detached" position="bottom" opened hideDetached={false}>
            <Button>Stays visible</Button>
          </Tooltip>
        </Group>
      </Box>
    </Box>
  );
}

Notifications auto close progress

Notifications now support withAutoCloseProgress prop. When set, a progress line that fills up until the notification auto closes is displayed at the bottom of every notification. The line fades out while the auto close timer is paused (for example, when the notification is hovered), starts over when the timer restarts, and uses the notification color. The prop sets the default for all notifications, it can be overridden for individual notifications with withAutoCloseProgress property in notifications.show and notifications.update functions:

import { Button, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';

function Demo() {
  return (
    <Group justify="center">
      <Button
        onClick={() =>
          notifications.show({
            title: 'Auto close progress',
            message: 'This notification will close in 5 seconds',
            withAutoCloseProgress: true,
            autoClose: 5000,
          })
        }
      >
        Show notification with progress
      </Button>

      <Button
        color="red"
        onClick={() =>
          notifications.show({
            title: 'Something went wrong',
            message: 'Progress line uses notification color',
            color: 'red',
            withAutoCloseProgress: true,
            autoClose: 8000,
          })
        }
      >
        Red notification with progress
      </Button>
    </Group>
  );
}

Notifications promise

New notifications.promise function displays a loading notification while a promise is pending and updates it with success or error state once the promise settles. success and error can be functions that receive the resolved value or the rejection reason:

import { Button, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';

function saveDocument(shouldFail: boolean) {
  return new Promise<{ name: string }>((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error('Network error'));
      } else {
        resolve({ name: 'report.pdf' });
      }
    }, 2000);
  });
}

function Demo() {
  const save = (shouldFail: boolean) =>
    notifications.promise(saveDocument(shouldFail), {
      loading: {
        title: 'Saving document',
        message: 'Please wait, it will take a couple of seconds',
      },
      success: (value) => ({
        title: 'Document saved',
        message: `${value.name} was saved successfully`,
      }),
      error: (error) => ({
        title: 'Failed to save document',
        message: (error as Error).message,
      }),
    });

  return (
    <Group justify="center">
      <Button onClick={() => save(false)}>Save document</Button>
      <Button color="red" onClick={() => save(true)}>
        Save document (fails)
      </Button>
    </Group>
  );
}

AppShell resizable sections

AppShell now supports resizing AppShell.Navbar/AppShell.Aside horizontally and AppShell.Header/AppShell.Footer vertically. Pass a useAppShellResize hook instance to the resize prop to enable it for the configured sections – handles support mouse, touch and keyboard, and can report drag-to-collapse. Each section accepts a label option to localize the aria-label of its handle. See AppShell examples for more details.

Note that when the resize prop is used, --app-shell-* CSS variables are defined on the AppShell root element instead of :root, so they cannot be read from content rendered in a Portal, for example from a Modal or a Drawer.

Templates and guides

Other changes

  • HoverCard now opens on keyboard focus, closes with Escape or a press outside, and is announced by screen readers. New events, interactive, role and returnFocus props, and onDismiss callback.
  • HoverCard no longer opens with a tap on touch devices. Set events={{ focus: false, touch: true }} and closeOnEscape={false} to restore the previous behavior.
  • Checkbox and Radio variant="light" colors are resolved with theme.variantColorResolver, so any CSS color or theme color with a shade can be used.
  • ActionBar with keepMounted now uses React Activity instead of display: none, so effects are paused while the bar is closed. Set transitionProps={{ keepMountedMode: 'display-none' }} to restore the previous behavior.
  • Mantine skills for AI coding agents (mantine-form, mantine-combobox, mantine-custom-components) have been updated for 9.x with corrected API details and new patterns.
  • Gatsby and Redwood guides have been removed, gatsby-template and redwood-template are no longer maintained. Existing applications can keep using Mantine, the setup does not change.
9 days ago
mantine

9.6.3

  • [@mantine/hooks] use-click-outside: Add capture phase support (#9213)
  • [@mantine/core] Select: Fix dropdown not closing on blur when searchable is not set (#9215)
  • [@mantine/core] PasswordInput: Fix aria-describedby not being taken from InputWrapperContext (#9216)
  • [@mantine/core] Fix aria-labelledby referencing a missing label element in Checkbox.Group, Radio.Group, Switch.Group (#9217)
  • [@mantine/core] Slider: Fix thumbProps not being passed to the thumb element (#9218)
  • [@mantine/core] Table: Fix memoized rows rerendering when Table rerenders with the same props (#6422)
  • [@mantine/hooks] use-intersection: Use the last entry from a batched observer callback (#9019)
  • [@mantine/hooks] use-media-query: Catch matchMedia errors when reading the initial value synchronously (#9020)
  • [@mantine/core] Portal: Cache shared portal node lookup (#9101)
  • [@mantine/schedule] MobileMonthView: Fix multi-day events not showing when they start in the previous month (#9153)
  • [@mantine/form] Fix removeListItem not clearing non-nested item errors (#9180)
  • [@mantine/core] Rating: Fix value being changed by scroll gestures on touch devices (#9159)
  • [@mantine/notifications] Fix uneven spacing in expanded stacks (#9176)
  • [@mantine/charts] Fix textColor and gridColor props being passed as attributes to the DOM node (#9181)
  • [@mantine/hooks] use-hotkeys: Fix preventDefault not being applied when other hotkey options are set (#9164)
  • [@mantine/hooks] use-throttled-callback: Fix callback becoming unresponsive after clearTimeout (#9146)
  • [@mantine/hooks] use-roving-index: Fix disabled items not being skipped by ArrowLeft/ArrowRight in grid mode (#9169)
  • [@mantine/hooks] use-mask: Fix transform not being applied to the initial input value (#9170)
14 days ago
mantine

9.6.2

  • [@mantine/core] Combobox: Fix incorrect aria-selected setting on up/down arrows press (#9205)
  • [@mantine/core] NumberInput: Fix min and max boundaries not being respected when trimLeadingZeroesOnBlur is false (#9192)
  • [@mantine/hooks] use-mask: Fix incorrect handling of input with mixed static values (#9196)
  • [@mantine/schedule] Add option to resize bacground events (#9190)
  • [@mantine/schedule] MobileMonthView: Fix missing localization for all day events label (#9191)
  • [@mantine/hooks] use-scroll-spy: Fix stale nodes being used when element cannot be resolved by the given selector
  • [@mantine/core] ComboboxPopover: Fix incorrect focus management
  • [@mantine/charts] GaugeChart: Fix incorrect roundCaps handling with multiple sections (#9189)
  • [@mantine/core] NumberInput: Fix incorrect thousands separator handling on paste (#9102)
25 days ago
mantine

9.6.1

  • [@mantine/ligtbox] Add option to render custom thumbnails for video
  • [@mantine/core] OverflowList: Fix ResizeObserver console warning (#9177)
  • [@mantine/core] Fix incorrect escape handling in Modal and Drawer stacks (#9188)
  • [@mantine/core] SegmentedControl: Fix indicator disappearing when data list is replaced with a list with the same length (#9175)
  • [@mantine/lightbox] Fix circular esm dependency (#9166)
  • [@mantine/core] Accordion: Fix incorrect border radius being applied to the contained items
  • [@mantine/schedule] Add event overlap mode support to WeekView and DayView
  • [@mantine/core] Menu: Fix incorrect withinPortal prop being available
  • [@mantine/core] Popover: Fix position not being updated when prop changes when the dropdown is opened (#9156)
  • [@mantine/schedule] Fix same-day events ending at exclusive midnight being dropped in WeekView and DayView components (#9157)
  • [@mantine/hooks] use-interval: Fix timer restarting when fn changes (#9158)
  • [@mantine/core] Textarea: Fix ResizeObserver loop error when autosize textarea width changes (#9162)
  • [@mantine/hooks] use-collapse: Fix incorrect transition state switching when used with Activity (#9160)
2026-08-31 23:15:50
mantine

9.6.0

View changelog with demos on mantine.dev website

Support Mantine development

You can now sponsor Mantine development with OpenCollective. All funds are used to improve Mantine and create new features and components.

@mantine/lightbox package

New @mantine/lightbox package – a full-screen media lightbox with carousel navigation, zoom, thumbnails, toolbar customization, and store-based API. Supports image, video, and custom slides:

import '@mantine/lightbox/styles.css';
import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
      />

      <SimpleGrid cols={3}>
        {images.map((src, i) => (
          <Image
            key={src}
            src={src}
            radius="md"
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setIndex(i);
              setOpened(true);
            }}
          />
        ))}
      </SimpleGrid>
    </>
  );
}

Key features:

  • Zoom – click to zoom on desktop, double-tap on mobile, scroll wheel and pinch gestures
  • Thumbnails – bottom thumbnail strip with active indicator
  • Store API – mount once, open from anywhere (same pattern as Spotlight and Notifications)
  • Video slides – native video player with auto-pause on navigation
  • Custom slides – render anything with custom thumbnails
  • Transitions – animated open and close with configurable transitionProps (same API as Modal)
  • Keyboard shortcuts – Escape, arrows, F/T/Z for fullscreen/thumbnails/zoom
  • Localization – every string is defined in the labels prop

Notifications custom rendering

Notifications now support renderNotification prop that allows you to completely replace the default notification with custom content. All animations (enter, exit, drag dismiss) are preserved for custom notifications:

import { Avatar, Button, Group, rem, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';

function Demo() {
  return (
    <Group justify="center">
      <Button
        onClick={() =>
          notifications.show({
            autoClose: false,
            renderNotification: (notification) => (
              <div
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: rem(12),
                  padding: rem(16),
                  borderRadius: rem(8),
                  backgroundColor: 'var(--mantine-color-body)',
                  border: '1px solid var(--mantine-color-default-border)',
                  boxShadow: 'var(--mantine-shadow-lg)',
                  userSelect: 'none',
                }}
              >
                <Avatar src={null} radius="xl" color="blue">
                  DM
                </Avatar>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <Text size="sm" fw={600}>
                    Dan sent you a message
                  </Text>
                  <Text size="xs" c="dimmed" lineClamp={1}>
                    Hey, are you free for a quick call?
                  </Text>
                  <Group gap="xs" mt={8}>
                    <Button
                      size="compact-xs"
                      variant="filled"
                      onClick={() =>
                        notifications.hide(notification.id!)
                      }
                    >
                      Reply
                    </Button>
                    <Button
                      size="compact-xs"
                      variant="default"
                      onClick={() =>
                        notifications.hide(notification.id!)
                      }
                    >
                      Dismiss
                    </Button>
                  </Group>
                </div>
              </div>
            ),
            message: '',
          })
        }
      >
        Show custom notification
      </Button>
    </Group>
  );
}

Notifications stacked layout

Notifications now support layout="stacked" prop that displays notifications in a stacked layout where only the latest notification is fully visible, and older notifications peek out behind it:

import { Button, Group } from '@mantine/core';
import { Notifications, notifications } from '@mantine/notifications';

function Demo() {
  return (
    <>
      {/* Replace your existing Notifications with layout="stacked" */}
      <Notifications layout="stacked" />
      <Group justify="center">
        <Button
          onClick={() => {
            notifications.show({
              title: 'New notification',
              message: 'This notification is part of a stacked layout',
            });
          }}
        >
          Show stacked notification
        </Button>
      </Group>
    </>
  );
}

ActionBar component

New ActionBar component – a fixed-position bottom bar for bulk selection actions. Designed to be controlled by table or checkbox selections, it provides a set of actions that can be performed on selected items.

import { useState } from 'react';
import { ActionBar, Button, Checkbox, Table, Text } from '@mantine/core';

const elements = [
  { position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' },
  { position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' },
  { position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' },
  { position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' },
  { position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' },
];

function Demo() {
  const [selection, setSelection] = useState<number[]>([]);

  const toggleRow = (position: number) =>
    setSelection((current) =>
      current.includes(position)
        ? current.filter((item) => item !== position)
        : [...current, position]
    );

  const toggleAll = () =>
    setSelection((current) =>
      current.length === elements.length ? [] : elements.map((element) => element.position)
    );

  const rows = elements.map((element) => (
    <Table.Tr
      key={element.position}
      bg={selection.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
    >
      <Table.Td>
        <Checkbox
          aria-label="Select row"
          checked={selection.includes(element.position)}
          onChange={() => toggleRow(element.position)}
        />
      </Table.Td>
      <Table.Td>{element.position}</Table.Td>
      <Table.Td>{element.name}</Table.Td>
      <Table.Td>{element.symbol}</Table.Td>
      <Table.Td>{element.mass}</Table.Td>
    </Table.Tr>
  ));

  return (
    <>
      <Table>
        <Table.Thead>
          <Table.Tr>
            <Table.Th>
              <Checkbox
                aria-label="Select all"
                checked={selection.length === elements.length}
                indeterminate={selection.length > 0 && selection.length !== elements.length}
                onChange={toggleAll}
              />
            </Table.Th>
            <Table.Th>Element position</Table.Th>
            <Table.Th>Element name</Table.Th>
            <Table.Th>Symbol</Table.Th>
            <Table.Th>Atomic mass</Table.Th>
          </Table.Tr>
        </Table.Thead>
        <Table.Tbody>{rows}</Table.Tbody>
      </Table>

      <ActionBar opened={selection.length > 0} onClose={() => setSelection([])} shadow="md">
        <Text size="sm">{selection.length} selected</Text>
        <ActionBar.Divider />
        <Button variant="default" size="compact-sm">
          Delete
        </Button>
        <Button variant="default" size="compact-sm">
          Move
        </Button>
        <Button variant="default" size="compact-sm">
          Archive
        </Button>
        <ActionBar.CloseButton />
      </ActionBar>
    </>
  );
}

RichTextEditor table controls

RichTextEditor now includes a set of controls for editing tables. Install and register the Tiptap table extension (TableKit), then add the controls to the toolbar. RichTextEditor.TableInsert opens a grid to pick the table size, and the other controls add/remove rows and columns, toggle header row/column and merge/split cells. All table controls are automatically disabled when the cursor is not inside a table:

import { TableKit } from '@tiptap/extension-table';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';

function Demo() {
  const editor = useEditor({
    extensions: [StarterKit, TableKit],
    content: `
      <table>
        <tbody>
          <tr><th><p>Framework</p></th><th><p>Language</p></th></tr>
          <tr><td><p>Mantine</p></td><td><p>TypeScript</p></td></tr>
          <tr><td><p>Tiptap</p></td><td><p>TypeScript</p></td></tr>
        </tbody>
      </table>
      <p></p>
`,
  });

  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar sticky>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.TableInsert />
          <RichTextEditor.TableDelete />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.TableColumnBefore />
          <RichTextEditor.TableColumnAfter />
          <RichTextEditor.TableColumnDelete />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.TableRowBefore />
          <RichTextEditor.TableRowAfter />
          <RichTextEditor.TableRowDelete />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.TableToggleHeaderRow />
          <RichTextEditor.TableToggleHeaderColumn />
          <RichTextEditor.TableMergeCells />
          <RichTextEditor.TableSplitCell />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor>
  );
}

RichTextEditor Details control

RichTextEditor now supports collapsible sections. Install and register the Tiptap details extension (Details, DetailsSummary and DetailsContent), then add RichTextEditor.Details to the toolbar. The control wraps the current block in a collapsible details node, or removes it when the cursor is already inside one:

import { Details, DetailsSummary, DetailsContent } from '@tiptap/extension-details';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';

function Demo() {
  const editor = useEditor({
    extensions: [StarterKit, Details, DetailsSummary, DetailsContent],
    content: `
      <details>
        <summary>Shipping and delivery</summary>
        <p>Orders are processed within 1–2 business days and delivered in 3–5 business days.</p>
      </details>
      <details>
        <summary>Returns and refunds</summary>
        <p>You can return any item within 30 days of delivery for a full refund.</p>
      </details>
      <p></p>
`,
  });

  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar sticky>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Bold />
          <RichTextEditor.Italic />
          <RichTextEditor.Underline />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Details />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor>
  );
}

To support the control, Typography now styles details and summary elements – a border, padding and a custom disclosure triangle. This applies to all details elements inside Typography, not just those created by the editor. All of the new selectors have zero specificity (:where()), so they can be overridden without !important.

RichTextEditor InvisibleCharacters control

RichTextEditor can now display formatting marks. Install and register the Tiptap invisible characters extension, then add RichTextEditor.InvisibleCharacters to the toolbar. The control toggles the visibility of spaces, paragraph breaks and hard breaks, and reflects the current visibility as its active state:

import InvisibleCharacters from '@tiptap/extension-invisible-characters';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';

function Demo() {
  const editor = useEditor({
    extensions: [StarterKit, InvisibleCharacters.configure({ visible: false })],
    content: `
      <p>Toggle the control to reveal spaces and paragraph breaks.</p>
      <p>Each space becomes a dot and every paragraph ends with a pilcrow.</p>
`,
  });

  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar sticky>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Bold />
          <RichTextEditor.Italic />
          <RichTextEditor.Underline />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.InvisibleCharacters />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor>
  );
}

GaugeChart component

New GaugeChart component – a radial gauge chart for KPI and status display. Supports threshold sections, target marker, custom labels, and configurable arc angles.

import { GaugeChart } from '@mantine/charts';

function Demo() {
  return <GaugeChart value={72} size={200} thickness={12} />;
}

WaffleChart component

New WaffleChart component – a part-to-whole grid chart with colored cells. Simpler and more compact alternative to pie/donut charts for displaying percentages and proportions.

// Demo.tsx
import { WaffleChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return <WaffleChart data={data} />;
}

// data.ts
import { WaffleChartCell } from '@mantine/charts';

export const data: WaffleChartCell[] = [
  { name: 'Chrome', value: 65, color: 'blue' },
  { name: 'Safari', value: 19, color: 'teal' },
  { name: 'Firefox', value: 10, color: 'orange' },
  { name: 'Other', value: 6, color: 'gray' },
];

MatrixChart component

New MatrixChart component – a generic x/y heatmap with categorical axes. Each cell is colored based on a value, useful for visualizing patterns in two-dimensional categorical data.

// Demo.tsx
import { MatrixChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <MatrixChart
      data={data}
      yLabels={['James', 'Mary', 'Robert', 'Linda', 'Michael', 'Sarah', 'David', 'Emma']}
      withYLabels
      withTooltip
      getTooltipLabel={({ x, y, value }) =>
        `${y}, Mar ${x}: ${value === null ? 'No contributions' : `${value} contribution${value > 1 ? 's' : ''}`}`
      }
    />
  );
}

// data.ts
import { MatrixChartCell } from '@mantine/charts';

export const data: MatrixChartCell[] = [
  { x: '1', y: 'James', value: 7 },
  { x: '2', y: 'James', value: 10 },
  { x: '3', y: 'James', value: 2 },
  { x: '4', y: 'James', value: 10 },
  { x: '5', y: 'James', value: 8 },
  { x: '6', y: 'James', value: null },
  { x: '7', y: 'James', value: null },
  { x: '8', y: 'James', value: 6 },
  { x: '9', y: 'James', value: 2 },
  { x: '10', y: 'James', value: 8 },
  { x: '11', y: 'James', value: 1 },
  { x: '12', y: 'James', value: 3 },
  { x: '13', y: 'James', value: 7 },
  { x: '14', y: 'James', value: null },
  { x: '15', y: 'James', value: 9 },
  { x: '16', y: 'James', value: 10 },
  { x: '17', y: 'James', value: null },
  { x: '18', y: 'James', value: 1 },
  { x: '19', y: 'James', value: 8 },
  { x: '20', y: 'James', value: null },
  { x: '21', y: 'James', value: null },
  { x: '22', y: 'James', value: 5 },
  { x: '23', y: 'James', value: 8 },
  { x: '24', y: 'James', value: 2 },
  { x: '25', y: 'James', value: 5 },
  { x: '26', y: 'James', value: 6 },
  { x: '27', y: 'James', value: null },
  { x: '28', y: 'James', value: null },
  { x: '29', y: 'James', value: 7 },
  { x: '30', y: 'James', value: 7 },
  { x: '31', y: 'James', value: 6 },
  { x: '1', y: 'Mary', value: 3 },
  { x: '2', y: 'Mary', value: 1 },
  // ... remaining data
];

CandlestickChart component

New CandlestickChart component – a financial OHLC chart that displays open, high, low and close values as candles. Candles are colored based on their direction, the wick shows the high–low range and the body shows the open–close range. Supports custom colors, data keys, reference lines, axis labels, tooltip labels and value formatting.

// Demo.tsx
import { CandlestickChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return <CandlestickChart h={300} data={data} dataKey="date"  tickLine="y" gridAxis="x" withXAxis={true} withYAxis={true} withTooltip={true} />;
}

// data.ts
export const data = [
  { date: 'Mar 01', open: 136, high: 142, low: 133, close: 140 },
  { date: 'Mar 02', open: 140, high: 145, low: 138, close: 139 },
  { date: 'Mar 03', open: 139, high: 141, low: 129, close: 131 },
  { date: 'Mar 04', open: 131, high: 134, low: 124, close: 125 },
  { date: 'Mar 05', open: 125, high: 133, low: 124, close: 132 },
  { date: 'Mar 06', open: 132, high: 138, low: 131, close: 137 },
  { date: 'Mar 07', open: 137, high: 137, low: 128, close: 129 },
  { date: 'Mar 08', open: 129, high: 135, low: 127, close: 134 },
  { date: 'Mar 09', open: 134, high: 148, low: 133, close: 146 },
  { date: 'Mar 10', open: 146, high: 152, low: 144, close: 151 },
  { date: 'Mar 11', open: 151, high: 154, low: 143, close: 145 },
  { date: 'Mar 12', open: 145, high: 149, low: 142, close: 148 },
  { date: 'Mar 13', open: 148, high: 156, low: 147, close: 155 },
  { date: 'Mar 14', open: 155, high: 158, low: 150, close: 152 },
  { date: 'Mar 15', open: 152, high: 153, low: 141, close: 143 },
  { date: 'Mar 16', open: 143, high: 147, low: 139, close: 146 },
  { date: 'Mar 17', open: 146, high: 160, low: 145, close: 159 },
  { date: 'Mar 18', open: 159, high: 164, low: 156, close: 157 },
  { date: 'Mar 19', open: 157, high: 162, low: 153, close: 161 },
  { date: 'Mar 20', open: 161, high: 168, low: 160, close: 166 },
];

Charts reference areas

AreaChart, BarChart, LineChart, CompositeChart and ScatterChart now support the referenceAreas prop that highlights a rectangular region of the plot – a weekend band, a target range, a threshold zone and similar annotations. Each area is bounded by x1/x2 and/or y1/y2 data values (omit one pair to span the full opposite axis) and supports a theme color and a label.

// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <AreaChart
      h={300}
      data={data}
      dataKey="date"
      type="stacked"
      series={[
        { name: 'Apples', color: 'indigo.6' },
        { name: 'Oranges', color: 'blue.6' },
        { name: 'Tomatoes', color: 'teal.6' },
      ]}
      referenceAreas={[
        { x1: 'Mar 23', x2: 'Mar 25', color: 'red.6', label: 'Weekend' },
      ]}
    />
  );
}

// data.ts
export const data = [
  {
    date: 'Mar 22',
    Apples: 2890,
    Oranges: 2338,
    Tomatoes: 2452,
  },
  {
    date: 'Mar 23',
    Apples: 2756,
    Oranges: 2103,
    Tomatoes: 2402,
  },
  {
    date: 'Mar 24',
    Apples: 3322,
    Oranges: 986,
    Tomatoes: 1821,
  },
  {
    date: 'Mar 25',
    Apples: 3470,
    Oranges: 2108,
    Tomatoes: 2809,
  },
  {
    date: 'Mar 26',
    Apples: 3129,
    Oranges: 1726,
    Tomatoes: 2290,
  },
];

Charts reference dots

AreaChart, BarChart, LineChart, CompositeChart and ScatterChart now support the referenceDots prop that marks individual points on the plot – a peak, an event, a record value or an anomaly. Each dot is positioned by x/y data coordinates and supports a radius, a theme color and a label. Reference dots are rendered on top of the chart series.

// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <AreaChart
      h={300}
      data={data}
      dataKey="date"
      series={[
        { name: 'Apples', color: 'indigo.6' },
        { name: 'Oranges', color: 'blue.6' },
        { name: 'Tomatoes', color: 'teal.6' },
      ]}
      referenceDots={[
        { x: 'Mar 25', y: 3470, color: 'red.6', label: 'Peak' },
      ]}
    />
  );
}

// data.ts
export const data = [
  {
    date: 'Mar 22',
    Apples: 2890,
    Oranges: 2338,
    Tomatoes: 2452,
  },
  {
    date: 'Mar 23',
    Apples: 2756,
    Oranges: 2103,
    Tomatoes: 2402,
  },
  {
    date: 'Mar 24',
    Apples: 3322,
    Oranges: 986,
    Tomatoes: 1821,
  },
  {
    date: 'Mar 25',
    Apples: 3470,
    Oranges: 2108,
    Tomatoes: 2809,
  },
  {
    date: 'Mar 26',
    Apples: 3129,
    Oranges: 1726,
    Tomatoes: 2290,
  },
];

Note that referenceLines in AreaChart are now rendered on top of the areas instead of behind them, which makes them consistent with BarChart, LineChart, CompositeChart and ScatterChart, where reference lines were already painted over the series.

AreaChart streamgraph

AreaChart now supports type="stream" that renders a streamgraph (also known as ThemeRiver) – a stacked area chart whose baseline flows around a central axis instead of being fixed to zero, producing the characteristic organic "river" shape. The y-axis is hidden by default for this type since its floating baseline makes the values not meaningful to read off:

// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <AreaChart
      h={300}
      data={data}
      dataKey="month"
      type="stream"
      series={[
        { name: 'Apples', color: 'indigo.6' },
        { name: 'Oranges', color: 'blue.6' },
        { name: 'Tomatoes', color: 'teal.6' },
        { name: 'Grapes', color: 'grape.6' },
      ]}
    />
  );
}

// data.ts
export const data = [
  { month: 'Jan', Apples: 220, Oranges: 140, Tomatoes: 90, Grapes: 60 },
  { month: 'Feb', Apples: 260, Oranges: 180, Tomatoes: 120, Grapes: 90 },
  { month: 'Mar', Apples: 300, Oranges: 240, Tomatoes: 180, Grapes: 140 },
  { month: 'Apr', Apples: 340, Oranges: 320, Tomatoes: 260, Grapes: 200 },
  { month: 'May', Apples: 380, Oranges: 420, Tomatoes: 360, Grapes: 280 },
  { month: 'Jun', Apples: 420, Oranges: 520, Tomatoes: 460, Grapes: 360 },
  { month: 'Jul', Apples: 400, Oranges: 560, Tomatoes: 520, Grapes: 420 },
  { month: 'Aug', Apples: 360, Oranges: 520, Tomatoes: 560, Grapes: 460 },
  { month: 'Sep', Apples: 300, Oranges: 440, Tomatoes: 520, Grapes: 420 },
  { month: 'Oct', Apples: 260, Oranges: 340, Tomatoes: 440, Grapes: 360 },
  { month: 'Nov', Apples: 220, Oranges: 260, Tomatoes: 340, Grapes: 280 },
  { month: 'Dec', Apples: 200, Oranges: 200, Tomatoes: 260, Grapes: 200 },
];

ScatterChart right Y axis

ScatterChart now supports the withRightYAxis prop that displays an additional Y axis on the right side of the chart, configurable with rightYAxisProps and rightYAxisLabel. Bind data series to the right Y axis by setting yAxisId: 'right' in the data object – series without yAxisId are bound to the left Y axis. Both axes use the same dataKey.y value, but their scales are calculated independently from the series assigned to them:

// Demo.tsx
import { ScatterChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <ScatterChart
      h={350}
      data={data}
      dataKey={{ x: 'month', y: 'value' }}
      withLegend
      withRightYAxis
      xAxisLabel="Month"
      yAxisLabel="Revenue"
      rightYAxisLabel="Conversion rate"
      rightYAxisProps={{ unit: '%' }}
    />
  );
}

// data.ts
export const data = [
  {
    color: 'indigo.6',
    name: 'Revenue',
    data: [
      { month: 1, value: 1200 },
      { month: 2, value: 1400 },
      { month: 3, value: 1350 },
      { month: 4, value: 1800 },
      { month: 5, value: 2100 },
      { month: 6, value: 1950 },
      { month: 7, value: 2400 },
      { month: 8, value: 2650 },
      { month: 9, value: 2300 },
      { month: 10, value: 2800 },
      { month: 11, value: 3100 },
      { month: 12, value: 3400 },
    ],
  },
  {
    color: 'teal.6',
    name: 'Conversion rate',
    yAxisId: 'right',
    data: [
      { month: 1, value: 3.4 },
      { month: 2, value: 3.9 },
      { month: 3, value: 3.1 },
      { month: 4, value: 4.2 },
      { month: 5, value: 4.8 },
      { month: 6, value: 4.1 },
      { month: 7, value: 5.3 },
      { month: 8, value: 5.9 },
      { month: 9, value: 5.1 },
      { month: 10, value: 6.2 },
      { month: 11, value: 6.8 },
      { month: 12, value: 7.4 },
    ],
  },
];

Stepper labelPosition

Stepper component now supports the labelPosition prop. Set labelPosition="bottom" to display the step label and description below the step icon:

import { useState } from 'react';
import { Stepper } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(1);
  return (
    <Stepper active={active} onStepClick={setActive} labelPosition="bottom">
      <Stepper.Step label="Account" />
      <Stepper.Step label="Verification" />
      <Stepper.Step label="Access" />
    </Stepper>
  );
}

Cascader safe area polygon

Cascader with expandTrigger="hover" now keeps the open column in place while the cursor moves diagonally toward it – options that the cursor passes over on the way no longer replace it. Set safeAreaPolygon={false} to expand on every hover immediately, or pass an object to configure Floating UI safePolygon options:

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      expandTrigger="hover"
      safeAreaPolygon={false}
      label="Location"
      placeholder="Hover to expand"
      data={data}
    />
  );
}

YearView renderDay

YearView now supports the renderDay prop that replaces the entire content of a day cell. The function is called with the day date in YYYY-MM-DD format and the events grouped on that day – the same list that is used to render the default indicators, but without the three items limit. This makes it possible to display counts, badges or icons instead of the default dots:

// Demo.tsx
import dayjs from 'dayjs';
import { YearView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <YearView
      date={new Date()}
      events={events}
      renderDay={(date, dayEvents) => (
        <>
          {dayjs(date).date()}

          {dayEvents.length > 0 && (
            <div
              style={{
                position: 'absolute',
                bottom: 0,
                insetInlineEnd: 0,
                minWidth: 12,
                height: 12,
                borderRadius: 12,
                fontSize: 9,
                lineHeight: '12px',
                fontWeight: 700,
                textAlign: 'center',
                color: 'var(--mantine-color-white)',
                backgroundColor: `var(--mantine-color-${dayEvents[0].color}-filled)`,
              }}
            >
              {dayEvents.length}
            </div>
          )}
        </>
      )}
    />
  );
}

ResourcesMonthView event resize

ResourcesMonthView now supports the withEventResize prop. Events can be resized by dragging their start or end edges, and the onEventResize callback is called with the updated event start and end dates. Resizing snaps to whole days and preserves the event's original time of day. Use canResizeEvent to control which events can be resized:

// Demo.tsx
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);

  return (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      resources={resources}
      events={events}
      withEventResize
      onEventResize={({ eventId, newStart, newEnd }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd }
              : event
          )
        );
      }}
      startScrollDate={dayjs().format('YYYY-MM-DD')}
    />
  );
}

// data.ts
import dayjs from 'dayjs';
import { ScheduleResourceData } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DD');
const nextWeek = dayjs().add(5, 'day').format('YYYY-MM-DD');

const resources: ScheduleResourceData[] = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
  { id: 'new-york', label: 'Meeting room: New York' },
];

const events = [
  {
    id: 1,
    title: 'Team Standup',
    start: \`\${today} 09:00:00\`,
    end: \`\${today} 09:30:00\`,
    color: 'blue',
    resourceId: 'tokyo',
  },
  {
    id: 2,
    title: 'Sprint Planning',
    start: \`\${today} 10:00:00\`,
    end: \`\${today} 11:30:00\`,
    color: 'green',
    resourceId: 'paris',
  },
  {
    id: 3,
    title: 'Design Review',
    start: \`\${tomorrow} 13:00:00\`,
    end: \`\${tomorrow} 14:00:00\`,
    color: 'orange',
    resourceId: 'tokyo',
  },
  {
    id: 4,
    title: 'Client Call',
    start: \`\${tomorrow} 09:30:00\`,
    end: \`\${tomorrow} 10:30:00\`,
    color: 'violet',
    resourceId: 'new-york',
  },
  {
    id: 5,
    title: 'Workshop',
    start: \`\${nextWeek} 14:00:00\`,
    end: \`\${nextWeek} 16:00:00\`,
    color: 'pink',
    resourceId: 'paris',
  },
];

Schedule drag and resize intervals

Time-grid Schedule views (DayView, WeekView, ResourcesDayView, ResourcesWeekView) now support eventDragInterval and eventResizeInterval props that set the snap step used when events are moved and resized, independent of the intervalMinutes grid size. For example, a 30-minute grid can allow 15-minute drag and resize increments. A ghost preview shows where the event will land while dragging:

import { useState } from 'react';
import dayjs from 'dayjs';
import { DayView, ScheduleEventData } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
  {
    id: 2,
    title: 'Team Meeting',
    start: `${today} 11:00:00`,
    end: `${today} 12:00:00`,
    color: 'green',
  },
  {
    id: 3,
    title: 'Code Review',
    start: `${today} 14:00:00`,
    end: `${today} 15:00:00`,
    color: 'violet',
  },
];

function Demo() {
  const [events, setEvents] = useState(initialEvents);

  const handleEventDrop = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
      )
    );
  };

  return (
    <DayView
      date={new Date()}
      events={events}
      startTime="08:00:00"
      endTime="18:00:00"
      intervalMinutes={30}
      eventDragInterval={15}
      withSubHourGridLines={false}
      withEventsDragAndDrop
      onEventDrop={handleEventDrop}
    />
  );
}
import { useState } from 'react';
import dayjs from 'dayjs';
import { DayView, ScheduleEventData } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
  {
    id: 2,
    title: 'Team Meeting',
    start: `${today} 11:00:00`,
    end: `${today} 12:00:00`,
    color: 'green',
  },
  {
    id: 3,
    title: 'Code Review',
    start: `${today} 14:00:00`,
    end: `${today} 15:00:00`,
    color: 'violet',
  },
];

function Demo() {
  const [events, setEvents] = useState(initialEvents);

  const handleEventResize = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
      )
    );
  };

  return (
    <DayView
      date={new Date()}
      events={events}
      startTime="08:00:00"
      endTime="18:00:00"
      intervalMinutes={30}
      eventResizeInterval={15}
      withSubHourGridLines={false}
      withEventResize
      onEventResize={handleEventResize}
    />
  );
}

Dropzone react-dropzone 20

Dropzone now depends on react-dropzone 20 (previously 15). The upgrade brings several behavior and type changes:

  • maxFiles no longer rejects the entire batch when more files are picked than the limit allows. Files up to the limit are now accepted and the rest are rejected. For example, picking 3 files with maxFiles={2} calls onDrop with the first 2 files and onReject with the third – previously all 3 files were rejected and onDrop was called with an empty array.
  • FileWithPath type now has required path and relativePath properties, they were optional before. The default file aggregator always sets both values, so onDrop files can be read without optional chaining. If you provide a custom getFilesFromEvent that returns plain File objects, these properties are not set at runtime.
  • getFilesFromEvent prop now receives DropEvent | FileSystemFileHandle[] instead of DropEvent – the File System Access API path passes file handles to the aggregator. Update the parameter type of custom aggregators to accept both.
  • react-dropzone 20 requires Node.js 22 or later. Mantine now requires Node.js 22 as well – Node.js 20 reached end of life in April 2026. This affects your development environment only, browser support is not changed.

Other changes

  • ColorInput now supports fullWidth prop: the dropdown matches the width of the input and the color picker inside it fills the available space.
  • FloatingWindow now supports onSizeChange, onResizeStart and onResizeEnd callbacks that mirror onPositionChange, onDragStart and onDragEnd used for dragging. Sizes passed to onSizeChange are measured after the new size has been applied, so they are already clamped by the dimensions and viewport constraints.
  • PasswordInput now supports visibilityToggleFocusable prop that puts the visibility toggle in the tab order: the button receives tabindex="0" and can be activated with Enter or Space.
  • Schedule views (DayView, WeekView, MonthView, ResourcesDayView, ResourcesWeekView) now support withInteractiveBackgroundEvents prop – background events (display: 'background') become clickable and trigger onEventClick, which makes it possible to open an edit modal for unavailability blocks and similar events.
  • use-scroll-spy hook scrollHost option now accepts a ref object in addition to a resolved HTMLElement – the hook reads ref.current internally once the element is mounted, so the scroll host does not need to exist on the first render.
  • YearView now supports withWeekendDays prop. Set withWeekendDays={false} to hide weekend days – every month grid shrinks to the remaining columns and events that fall only on hidden days are not displayed.
2026-08-23 01:00:09
mantine

9.5.2

  • [@mantine/hooks] use-debounced-value: Fix leading: true firing multiple times per burst and emiting a stale value (#9119)
  • [@mantine/schedule] Fix recurring events not working with timzones (#9112)
  • [@mantine/dates] Fix minDate used for default date in some cases (#9117)
  • [@mantine/core] Tooltip: Fix tooltip setting NaN in top/left position style when event position values cannot be read (#9131)
  • [@mantine/dates] TimePicker: Fix incorrect focus handling of partially filled hours field (#9128)
  • [@mantine/core] RollingNumber: Fix incorrect copy event handling (#9132)
  • [@mantine/core] Notification: Fix incorrect closeButtonProps type (#9134)
  • [@mantine/code-highlight] Add support for lazy languages loading (#9141)
  • [@mantine/code-highlight] CodeHighlight: Add prop to keep indentation of the first line of the code block (#9140)
  • [@mantine/dates] Add missing formatting functions to MiniCalendarm DateInput and YarsList components
  • [@mantine/schedule] WeekView: Improve performance of events positioning algorithm (#9075)
  • [@mantine/form] Add new useWatchValue hook
  • [@mantine/core] Fix Combobox-based components not working correctly with Chrome autocomplete
2026-08-02 15:37:47
mantine

9.5.1

  • [@mantine/tiptap] Fix controls being initially disabledbefore element is focused
  • [@mantine/tiptap] Fix source code control wrapping content with extra p tag
  • [@mantine/hooks] use-scroll-spy: Allow usage with refs (#9025)
  • [@mantine/core] ColorInput: Add support for fullWidth prop (#9061)
  • [@mantine/core] Checkbox: Fix incottect indeterminate aria attributes handling in Checkbox.Card (#9095)
  • [@mantine/core] FloatingIndicator: Fix position and size calculation under scaled ancestors (#9071)
  • [@mantine/core] Tooltip: Add interactive prop support (#9072)
  • [@mantine/core] Cascader: Add safe area polygon support
  • [@mantine/core] PasswordInput: Add option to change whether the visibility toggle is focusable (#9090)
  • [@mantine/charts] ScatterChart: Add option to add second y axis
  • [@mantine/schedule] YearView: Add renderDay prop support
  • [@mantine/schedule] YearView: Add option to hide weekend days
  • [@mantine/core] InputWrapper: Fix component: div triggering typescript error if passed to descriptionProps
  • [@mantine/schedule] ResourcesMonthView: Add option to resize events
  • [@mantine/core] FloatingWindow: Add support for onSizeChange and onResizeStart props (#9085)
2026-07-27 15:48:18
mantine

9.5.0 🤖

View changelog with demos on mantine.dev website

Support Mantine development

You can now sponsor Mantine development with OpenCollective. All funds are used to improve Mantine and create new features and components.

Migration to oxc

Mantine has migrated its linting and formatting toolchain from ESLint and Prettier to oxc – oxlint is now used as the linter and oxfmt as the formatter. Both tools are written in Rust and are significantly faster than their predecessors, which makes linting and formatting the entire codebase almost instant.

The shared configuration is available as a new oxc-config-mantine package (a replacement for the previous eslint-config-mantine). You can use it in your own projects to follow the same code style and conventions as Mantine.

Native level select in date pickers

DatePicker and all other date picker components (DatePickerInput, MonthPicker, YearPicker, DateTimePicker, etc.) now support the withNativeLevelSelect prop. When enabled, it replaces the calendar header level button with native <select> elements, making it easy to quickly navigate to a specific month and year.

import { DatePicker } from '@mantine/dates';

function Demo() {
  return <DatePicker withNativeLevelSelect yearsSelectRange={[2020, 2035]} />;
}

Timeline opposite and alternate content

Timeline Timeline.Item component now supports the opposite prop that allows rendering content on the opposite side of the timeline. When any item has the opposite prop, the timeline switches to a centered layout with content on both sides of the line.

import { Timeline, Text } from '@mantine/core';
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon, ChatCircleDotsIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <Timeline active={1} bulletSize={24} lineWidth={2}>
      <Timeline.Item
        bullet={<GitBranchIcon size={12} />}
        title="New branch"
        opposite={
          <Text size="sm" c="dimmed">
            2 hours ago
          </Text>
        }
      >
        <Text c="dimmed" size="sm">You&apos;ve created new branch <Text variant="link" component="span" inherit>fix-notifications</Text> from master</Text>
      </Timeline.Item>

      <Timeline.Item
        bullet={<GitCommitIcon size={12} />}
        title="Commits"
        opposite={
          <Text size="sm" c="dimmed">
            52 minutes ago
          </Text>
        }
      >
        <Text c="dimmed" size="sm">You&apos;ve pushed 23 commits to <Text variant="link" component="span" inherit>fix-notifications branch</Text></Text>
      </Timeline.Item>

      <Timeline.Item
        title="Pull request"
        bullet={<GitPullRequestIcon size={12} />}
        lineVariant="dashed"
        opposite={
          <Text size="sm" c="dimmed">
            34 minutes ago
          </Text>
        }
      >
        <Text c="dimmed" size="sm">You&apos;ve submitted a pull request <Text variant="link" component="span" inherit>Fix incorrect notification message (#187)</Text></Text>
      </Timeline.Item>

      <Timeline.Item title="Code review" bullet={<ChatCircleDotsIcon size={12} />}>
        <Text c="dimmed" size="sm"><Text variant="link" component="span" inherit>Robert Gluesticker</Text> left a code review on your pull request</Text>
      </Timeline.Item>
    </Timeline>
  );
}

Set the alternate prop on individual Timeline.Item components to switch the position of content and opposite:

import { Timeline, Text } from '@mantine/core';
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon, ChatCircleDotsIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <Timeline active={2} bulletSize={24} lineWidth={2}>
      <Timeline.Item
        bullet={<GitBranchIcon size={12} />}
        title="New branch"
        opposite={
          <Text size="sm" c="dimmed">
            2 hours ago
          </Text>
        }
      >
        <Text c="dimmed" size="sm">You&apos;ve created new branch <Text variant="link" component="span" inherit>fix-notifications</Text> from master</Text>
      </Timeline.Item>

      <Timeline.Item
        bullet={<GitCommitIcon size={12} />}
        title="Commits"
        opposite={
          <Text size="sm" c="dimmed">
            52 minutes ago
          </Text>
        }
        alternate
      >
        <Text c="dimmed" size="sm">You&apos;ve pushed 23 commits to <Text variant="link" component="span" inherit>fix-notifications branch</Text></Text>
      </Timeline.Item>

      <Timeline.Item
        title="Pull request"
        bullet={<GitPullRequestIcon size={12} />}
        lineVariant="dashed"
        opposite={
          <Text size="sm" c="dimmed">
            34 minutes ago
          </Text>
        }
      >
        <Text c="dimmed" size="sm">You&apos;ve submitted a pull request <Text variant="link" component="span" inherit>Fix incorrect notification message (#187)</Text></Text>
      </Timeline.Item>

      <Timeline.Item
        title="Code review"
        bullet={<ChatCircleDotsIcon size={12} />}
        opposite={
          <Text size="sm" c="dimmed">
            12 minutes ago
          </Text>
        }
        alternate
      >
        <Text c="dimmed" size="sm"><Text variant="link" component="span" inherit>Robert Gluesticker</Text> left a code review on your pull request</Text>
      </Timeline.Item>
    </Timeline>
  );
}

FloatingWindow resize handle

FloatingWindow now supports a ResizeHandle compound component that allows users to resize the floating window by dragging a handle element. Set the dimensions prop on FloatingWindow to control resize constraints for both width (initialWidth, minWidth, maxWidth) and height (initialHeight, minHeight, maxHeight).

The resize handle is fully accessible – it supports keyboard interaction with Arrow Left/Arrow Right keys for width, Arrow Up/Arrow Down for height (10px steps), and Home/End keys (jump to min/max size).

import { NotchesIcon } from '@phosphor-icons/react';
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
          withBorder
          constrainOffset={40}
          dimensions={{
            initialWidth: 260,
            maxWidth: 500,
            minWidth: 180,
            initialHeight: 260,
            maxHeight: 400,
            minHeight: 220,
          }}
          dragHandleSelector=".drag-handle"
          excludeDragHandleSelector="button"
          initialPosition={{ top: 300, left: 60 }}
          style={{ overflow: 'hidden' }}
        >
          <Group
            justify="space-between"
            px="md"
            py="sm"
            className="drag-handle"
            style={{ cursor: 'move' }}
          >
            <Text fw={500} fz="sm">
              Resize demo
            </Text>
            <CloseButton onClick={handlers.close} />
          </Group>
          <Text fz="sm" px="md" pb="sm">
            Drag the grip icon in the bottom-right corner to resize.
            Use Arrow keys when the handle is focused:
            Left/Right for width, Up/Down for height.
          </Text>
          <FloatingWindow.ResizeHandle
            aria-label="Resize floating window"
            style={{
              position: 'absolute',
              right: 0,
              bottom: 0,
              width: 20,
              height: 20,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              cursor: 'nwse-resize',
            }}
          >
            <NotchesIcon size={14} style={{ opacity: 0.5 }} />
          </FloatingWindow.ResizeHandle>
        </FloatingWindow>
      )}
    </>
  );
}

Cascader component

New Cascader component allows selecting a value from hierarchical data by drilling down through cascading columns. Picking an option in one column reveals its children in a new column to the right, and the value is an ordered path from the root option to the selected node. It supports changeOnSelect, hover expand trigger, search, a flat list layout for mobile, and full keyboard navigation.

import { Cascader, useMatches } from '@mantine/core';
import { data } from './data';

function Demo() {
  // Switch to a flat list on small screens
  const withColumns = useMatches({ base: false, sm: true });
  return (
    <Cascader
      withColumns={withColumns}
      label="Location"
      placeholder="Pick location"
      data={data}
    />
  );
}

SunburstChart component

New SunburstChart component displays hierarchical data as concentric rings, similar to a treemap plotted in polar coordinates.

// Demo.tsx
import { SunburstChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return <SunburstChart data={data} />;
}

// data.ts
export const data = [
  { name: 'Analytics', value: 100, color: 'pink.6' },
  {
    name: 'DevOps',
    color: 'grape.6',
    children: [
      { name: 'Docker', value: 80 },
      { name: 'Kubernetes', value: 50 },
    ],
  },
  {
    name: 'Backend',
    color: 'teal.6',
    children: [
      { name: 'Node', value: 150 },
      {
        name: 'Python',
        children: [
          { name: 'Django', value: 110 },
          { name: 'FastAPI', value: 60 },
        ],
      },
      { name: 'Go', value: 50 },
    ],
  },
  {
    name: 'Frontend',
    color: 'blue.6',
    children: [
      {
        name: 'React',
        children: [
          {
            name: 'Frameworks',
            children: [
              { name: 'Next.js', value: 150 },
              { name: 'Remix', value: 40 },
            ],
          },
          { name: 'CRA', value: 20 },
        ],
      },
      { name: 'Vue', value: 90 },
      { name: 'Svelte', value: 30 },
    ],
  },
];

BulletChart component

New BulletChart component displays a single measure against a qualitative range, useful for comparing a primary value (such as revenue) against a target and qualitative thresholds like poor, average, and good.

// Demo.tsx
import { BulletChart } from '@mantine/charts';
import { ranges } from './data';

function Demo() {
  return (
    <BulletChart
      value={230000}
      target={150000}
      ranges={ranges}
      valueFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
    />
  );
}

// data.ts
export const ranges = ${JSON.stringify(ranges, null, 2)};

Charts keyboard navigation

All @mantine/charts components now expose the accessibilityLayer prop (true by default) that makes charts navigable with the keyboard. The chart surface is focusable and displays the Mantine focus ring; once focused, the arrow keys move the active tooltip point-by-point and Enter toggles the tooltip, so users who do not use a mouse can read the underlying values. The prop is supported by AreaChart, BarChart, LineChart, CompositeChart, ScatterChart, BubbleChart, PieChart, DonutChart, RadarChart, RadialBarChart and FunnelChart components.

Charts brush

AreaChart, BarChart, LineChart and CompositeChart now support the withBrush prop that displays a brush (range selector) under the chart. Drag the brush handles to zoom into a subset of the data. Use the brushProps prop to customize the underlying recharts Brush, or render the new themed ChartBrush component as a child of the chart for full control.

// Demo.tsx
import { AreaChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return (
    <AreaChart
      h={300}
      data={data}
      dataKey="date"
      withBrush
      series={[
        { name: 'Apples', color: 'indigo.6' },
        { name: 'Oranges', color: 'blue.6' },
      ]}
    />
  );
}

// data.ts
export const data = [
  { date: 'Mar 1', Apples: 2200, Oranges: 1400 },
  { date: 'Mar 2', Apples: 2500, Oranges: 1500 },
  { date: 'Mar 3', Apples: 2800, Oranges: 1700 },
  { date: 'Mar 4', Apples: 3100, Oranges: 1600 },
  { date: 'Mar 5', Apples: 3000, Oranges: 1800 },
  { date: 'Mar 6', Apples: 2700, Oranges: 2000 },
  { date: 'Mar 7', Apples: 2400, Oranges: 2100 },
  { date: 'Mar 8', Apples: 2100, Oranges: 1900 },
  { date: 'Mar 9', Apples: 1900, Oranges: 1700 },
  { date: 'Mar 10', Apples: 2200, Oranges: 1500 },
  { date: 'Mar 11', Apples: 2600, Oranges: 1600 },
  { date: 'Mar 12', Apples: 3000, Oranges: 1800 },
  { date: 'Mar 13', Apples: 3300, Oranges: 2000 },
  { date: 'Mar 14', Apples: 3100, Oranges: 2200 },
  { date: 'Mar 15', Apples: 2800, Oranges: 2100 },
  { date: 'Mar 16', Apples: 2500, Oranges: 1900 },
];

Heatmap month labels position

Heatmap now supports monthLabelsPosition prop that allows displaying month labels at the bottom of the heatmap instead of the top (default).

// 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
      monthLabelsPosition="bottom"
    />
  );
}

// data.ts
export const data = ${JSON.stringify(data, null, 2)};

Accordion disable collapse

Accordion now supports the disableCollapse prop. When enabled in single mode (multiple={false}), the open item cannot be collapsed by clicking it again, so one item always stays open. Pair it with defaultValue or value to guarantee that a section is open from the start – useful for settings panels, stepper-style flows and FAQ pages.

// Demo.tsx
import { Accordion } from '@mantine/core';
import { data } from './data';

function Demo() {
  const items = data.map((item) => (
    <Accordion.Item key={item.value} value={item.value}>
      <Accordion.Control icon={item.emoji}>{item.value}</Accordion.Control>
      <Accordion.Panel>{item.description}</Accordion.Panel>
    </Accordion.Item>
  ));

  return (
    <Accordion defaultValue="Apples" disableCollapse order={3}>
      {items}
    </Accordion>
  );
}

// data.ts
export const data = [
  {
    emoji: '🍎',
    value: 'Apples',
    description:
      'Crisp and refreshing fruit. Apples are known for their versatility and nutritional benefits. They come in a variety of flavors and are great for snacking, baking, or adding to salads.',
  },
  {
    emoji: '🍌',
    value: 'Bananas',
    description:
      'Naturally sweet and potassium-rich fruit. Bananas are a popular choice for their energy-boosting properties and can be enjoyed as a quick snack, added to smoothies, or used in baking.',
  },
  {
    emoji: '🥦',
    value: 'Broccoli',
    description:
      'Nutrient-packed green vegetable. Broccoli is packed with vitamins, minerals, and fiber. It has a distinct flavor and can be enjoyed steamed, roasted, or added to stir-fries.',
  },
];

ScrollArea vertical scrollbar position

ScrollArea now supports the verticalScrollbarPosition prop that pins the vertical scrollbar to a physical side (left or right) regardless of direction. This is useful for RTL applications where users expect the vertical scrollbar to stay on the right, matching the behavior of most desktop software. The prop also realigns the offset padding, the corner and the horizontal scrollbar gap, so it works correctly with offsetScrollbars and scrollbars="xy".

import { ScrollArea } from '@mantine/core';

function Demo() {
  return (
    <ScrollArea
      w={300}
      h={200}
      type="always"
      scrollbars="y"
       verticalScrollbarPosition="right" offsetScrollbars={true}
    >
      {/* ... content */}
    </ScrollArea>
  );
}

Schedule intervals larger than one hour

ResourcesDayView and ResourcesWeekView now support intervalMinutes values larger than 60. Previously the interval was capped at one hour – you can now set it to any whole number of hours (for example 120 or 240) to display multi-hour time slots. Values of 60 or less must still divide evenly into an hour; any value that does not keep the grid on hour boundaries falls back to 60.

// Demo.tsx
import dayjs from 'dayjs';
import { useState } from 'react';
import { SegmentedControl, Stack } from '@mantine/core';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [intervalMinutes, setIntervalMinutes] = useState('120');

  return (
    <Stack>
      <SegmentedControl
        value={intervalMinutes}
        onChange={setIntervalMinutes}
        data={[
          { label: '1 hour', value: '60' },
          { label: '2 hours', value: '120' },
          { label: '4 hours', value: '240' },
        ]}
      />
      <ResourcesDayView
        date={date}
        onDateChange={setDate}
        resources={resources}
        events={events}
        intervalMinutes={Number(intervalMinutes)}
        startScrollTime="08:00:00"
      />
    </Stack>
  );
}

// data.ts
import dayjs from 'dayjs';
import { ScheduleResourceData } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');

const resources: ScheduleResourceData[] = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
  { id: 'new-york', label: 'Meeting room: New York' },
  { id: 'london', label: 'Meeting room: London' },
];

const events = [
  {
    id: 1,
    title: 'Team Standup',
    start: \`\${today} 09:00:00\`,
    end: \`\${today} 09:30:00\`,
    color: 'blue',
    resourceId: 'tokyo',
  },
  {
    id: 2,
    title: 'Sprint Planning',
    start: \`\${today} 10:00:00\`,
    end: \`\${today} 11:30:00\`,
    color: 'green',
    resourceId: 'tokyo',
  },
  {
    id: 3,
    title: 'Client Call',
    start: \`\${today} 09:30:00\`,
    end: \`\${today} 10:30:00\`,
    color: 'violet',
    resourceId: 'paris',
  },
  {
    id: 4,
    title: 'Design Review',
    start: \`\${today} 13:00:00\`,
    end: \`\${today} 14:00:00\`,
    color: 'orange',
    resourceId: 'paris',
  },
  {
    id: 5,
    title: '1:1 Meeting',
    start: \`\${today} 11:00:00\`,
    end: \`\${today} 11:30:00\`,
    color: 'cyan',
    resourceId: 'new-york',
  },
  {
    id: 6,
    title: 'Workshop',
    start: \`\${today} 14:00:00\`,
    end: \`\${today} 16:00:00\`,
    color: 'pink',
    resourceId: 'new-york',
  },
  {
    id: 7,
    title: 'Architecture Review',
    start: \`\${today} 10:00:00\`,
    end: \`\${today} 11:00:00\`,
    color: 'red',
    resourceId: 'london',
  },
  {
    id: 8,
    title: 'Retrospective',
    start: \`\${today} 15:00:00\`,
    end: \`\${today} 16:00:00\`,
    color: 'grape',
    resourceId: 'london',
  },
];

New example: Table virtualization

New Table example demonstrates how to use Table.ScrollContainer with @tanstack/react-virtual to efficiently render large datasets with 5,000 rows.

import { useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Table } from '@mantine/core';
import { generateData } from './data';

const data = generateData(5000);
const ROW_HEIGHT = 36;

function Demo() {
  const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);

  const virtualizer = useVirtualizer({
    count: data.length,
    getScrollElement: () => scrollParent,
    estimateSize: () => ROW_HEIGHT,
    overscan: 20,
  });

  const virtualItems = virtualizer.getVirtualItems();

  return (
    <Table.ScrollContainer
      minWidth={500}
      maxHeight={400}
      scrollAreaProps={{ viewportRef: setScrollParent }}
    >
      <Table stickyHeader layout="fixed">
        <Table.Thead>
          <Table.Tr>
            <Table.Th w={50}>#</Table.Th>
            <Table.Th>Name</Table.Th>
            <Table.Th>Email</Table.Th>
            <Table.Th>Company</Table.Th>
            <Table.Th>City</Table.Th>
          </Table.Tr>
        </Table.Thead>

        <Table.Tbody>
          {virtualItems.length > 0 && (
            <tr aria-hidden>
              <td
                aria-hidden
                colSpan={5}
                style={{ height: virtualItems[0].start, padding: 0, border: 'none' }}
              />
            </tr>
          )}

          {virtualItems.map((virtualItem) => {
            const row = data[virtualItem.index];
            return (
              <Table.Tr key={virtualItem.index}>
                <Table.Td>{row.id}</Table.Td>
                <Table.Td>{row.name}</Table.Td>
                <Table.Td>{row.email}</Table.Td>
                <Table.Td>{row.company}</Table.Td>
                <Table.Td>{row.city}</Table.Td>
              </Table.Tr>
            );
          })}

          {virtualItems.length > 0 && (
            <tr aria-hidden>
              <td
                aria-hidden
                colSpan={5}
                style={{
                  height:
                    virtualizer.getTotalSize() -
                    virtualItems[virtualItems.length - 1].end,
                  padding: 0,
                  border: 'none',
                }}
              />
            </tr>
          )}
        </Table.Tbody>
      </Table>
    </Table.ScrollContainer>
  );
}
2026-07-21 15:55:50
mantine

9.4.2

What's Changed

  • [@mantine/schedule] Fix 1-3 minutes events not aligning correctly in ResourcesWeekView and ResourcesDayView (#9057)
  • [@mantine/form] Fix async validation with debounce on initial keystroke of empty input (#9068)
  • [@mantine/core] Add keepMountedMode prop to Modal and Drawer components (#9056)
  • [@mantine/core] Menubar: Fix menu requiring two clicks to dismiss after switching menus with hover (#9050)
  • [@mantine/core] AppShell: Fix breakpoint={0} not working correctly (#9042)
  • [@mantine/core] Tree: Fix keyboard navigation not working on Safari (#9049)
  • [@mantine/notifications] NotificationContainer: Fix autoClose leak (#9048)
  • [@mantine/core] Collapse: Add keepMountedMode prop (#9021)
  • [@mantine/dates] TimePicker: Fix incorrect withSeconds handling when pasting partial values (#9041)
  • [@mantine/schedule] Add option to customize current time in ResourcesDayView and ResourcesWeekView components
  • [@mantine/core] TreeSelect: Add expand callback to renderNode payload
  • [@mantine/core] Fix incorrect empty children arrays handling in Tree and TreeSelect
  • [@mantine/hooks] Fix unstable scrollIntoView for some bundlers (#9035)
  • [@mantine/schedule] Add intervalMinutes more than 60 support to ResourcesDayView and ResourcesWeekView
  • [@mantine/schedule] ResourcesWeekView: Fix incorrect handling of events with small duration
  • [@mantine/schedule] ResourcesDayView: Fix events with small durations not being visible

New Contributors

Full Changelog: https://github.com/mantinedev/mantine/compare/9.4.1...9.4.2

2026-06-28 18:48:46
mantine

9.4.1

What's Changed

  • [@mantine/form] Fix some functions not working correctly with react compiler (#9007)
  • [@mantine/charts] Heatmap: Fix values outside the provided domain rendering with no fill (#8982)
  • [@mantine/core] RollingNumber: Fix rendering -0 for values that round to zero (#8983)
  • [@mantine/core] Slider: Fix incorrect marks labels position in RTL layouts (#8996)
  • [@mantine/hooks] Fix use-set and use-map hooks using stale values when used with React compiler (#9008)
  • [@mantine/form] Add FormProviderProps type export (#9009)
  • [@mantine/schedule] ResourcesDayView: Fix incorrect multiday events rendering (#9014)
  • [@mantine/dates] TimePicker: Fix duration values greater than 9999 hours not working (#9002)
  • [@mantine/core] Menu: Mark non-menuitem dropdown children as presentational (#9004)
  • [@mantine/core] Collapse: Fix ref and style props not working when transitionDuration: 0 (#9013)
  • [@mantine/core] Textarea: Fix autosize not working correctly with minRows on initial render
  • [@mantine/schedule] ResourcesWeekView: Add events resizing support

New Contributors

Full Changelog: https://github.com/mantinedev/mantine/compare/9.4.0...9.4.1