Version v9.6.0

Support Mantine development

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

Sponsor Mantine

@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.

Element positionElement nameSymbolAtomic mass
6CarbonC12.011
7NitrogenN14.007
39YttriumY88.906
56BariumBa137.33
58CeriumCe140.12
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.

72
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.

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

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

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.

JamesMaryRobertLindaMichaelSarahDavidEmma
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' : ''}`}`
      }
    />
  );
}

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.

Tick line
Grid axis
import { CandlestickChart } from '@mantine/charts';
import { data } from './data';

function Demo() {
  return <CandlestickChart h={300} data={data} dataKey="date"  />;
}

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.

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' },
      ]}
    />
  );
}

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.

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' },
      ]}
    />
  );
}

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:

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' },
      ]}
    />
  );
}

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:

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: '%' }}
    />
  );
}

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:

M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
M
T
W
T
F
S
S
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:

Resources
Sat1
Sun2
Mon3
Tue4
Wed5
Thu6
Fri7
Sat8
Sun9
Mon10
Tue11
Wed12
Thu13
Fri14
Sat15
Sun16
Mon17
Tue18
Wed19
Thu20
Fri21
Sat22
Sun23
Mon24
Tue25
Wed26
Thu27
Fri28
Sat29
Sun30
Mon31
Meeting room: Tokyo
Meeting room: Paris
Meeting room: New York
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')}
    />
  );
}

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:

All day
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
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}
    />
  );
}
All day
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
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.