DEV Community

Cover image for Mantine Split Pane v3 - Responsive Sizing, Container-Aware Panes, and a Leaner API
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

Mantine Split Pane v3 - Responsive Sizing, Container-Aware Panes, and a Leaner API

Introduction

Version 3.0.0 of @gfazioli/mantine-split-pane brings the same responsive-first philosophy you already enjoy with orientation to every size-related prop on panes and resizers. You can now pass Mantine breakpoint maps to initialWidth, minWidth, maxWidth, size, knobSize, spacing, and their height counterparts. On top of that, panes with percentage-based sizes automatically stay in proportion when the container is resized -- and if a user has manually dragged a resizer, the drag ratio is preserved across resize events.

This release also cleans up several internal inconsistencies, adds comprehensive JSDoc documentation to every public API surface, and exports a new ResponsiveValue<T> utility type so you can build your own responsive abstractions on the same foundation.

What's New

Responsive Pane Sizing

Every size prop on Split.Pane now accepts a Mantine breakpoint map in addition to static values. This means you can declare layouts that reshape themselves across viewports without any media-query boilerplate.

Affected props: initialWidth, initialHeight, minWidth, minHeight, maxWidth, maxHeight

import { Split } from '@gfazioli/mantine-split-pane';

<Split w="100%" h={320}>
  <Split.Pane
    initialWidth={{ base: '100%', md: '30%' }}
    minWidth={{ base: 100, lg: 200 }}
  >
    <Sidebar />
  </Split.Pane>

  <Split.Resizer />

  <Split.Pane grow>
    <MainContent />
  </Split.Pane>
</Split>
Enter fullscreen mode Exit fullscreen mode

On viewports below md, the sidebar requests 100% of the container width; from md upward it settles at 30%. The minimum width constraint similarly shifts from 100px to 200px at the lg breakpoint.

Under the hood, a new useResponsiveValue hook (built on Mantine's useMatches) resolves every breakpoint map to a scalar before the value reaches the DOM. If you pass a plain number or string, it flows through unchanged -- no performance overhead, full backward compatibility.

Responsive Resizer Props

The same breakpoint-map pattern is available on the resizer. size, knobSize, and spacing now accept ResponsiveValue<T>, letting you fine-tune the resizer chrome per breakpoint.

<Split
  size={{ base: 'xs', md: 'sm' }}
  knobSize={{ base: 12, lg: 18 }}
  spacing={{ base: 4, md: 8 }}
>
  {/* ... */}
</Split>
Enter fullscreen mode Exit fullscreen mode

These values are resolved both at the Split level (for context-cascaded defaults) and inside SplitResizer itself (for per-instance overrides), so the final rendered value is always a concrete scalar by the time it reaches the styles system.

Container Resize with Proportional Panes

Panes configured with percentage-based sizes (e.g. initialWidth="30%") now respond to container resizes automatically. A ResizeObserver on the root <Split> element tracks the container dimensions and pushes them into context. Each pane with a percentage size recalculates its pixel width or height whenever the container changes, respecting minWidth/maxWidth constraints along the way.

If a user has manually dragged a resizer, the component stores the drag position as a ratio of the container dimension. On subsequent container resizes, that ratio is preserved rather than snapping back to the original percentage -- so the user's intent is respected even as the window changes size.

<Split w="100%" h={320}>
  <Split.Pane initialWidth="30%" minWidth={100}>
    <LeftPanel />
  </Split.Pane>

  <Split.Resizer />

  <Split.Pane grow>
    <CenterPanel />
  </Split.Pane>

  <Split.Resizer />

  <Split.Pane initialWidth="20%" minWidth={80}>
    <RightPanel />
  </Split.Pane>
</Split>
Enter fullscreen mode Exit fullscreen mode

Resize the browser window and each percentage-based pane scales proportionally. Drag a resizer, then resize the window, and the dragged position scales with the container rather than resetting.

New ResponsiveValue<T> Type Export

A new generic utility type is exported from the package:

import type { ResponsiveValue } from '@gfazioli/mantine-split-pane';

// Equivalent to:
// T | Partial<Record<MantineBreakpoint | (string & {}), T>>
Enter fullscreen mode Exit fullscreen mode

Use it to type your own props or configuration objects that should support the same breakpoint-map pattern.

New useResponsiveValue Hook

While not exported from the public API (it is an internal hook), useResponsiveValue is the foundation underlying all responsive props. It wraps Mantine's useMatches and provides a clean scalar-or-breakpoint-map resolution pattern. The useSplitResizerOrientation hook has been refactored to delegate to it, eliminating duplicate logic.

Breaking Changes

Pane Handler Return Types Are Now Nullable

The imperative handlers exposed via ref on Split.Pane have updated return types to reflect reality more accurately.

Before (v2):

interface SplitPaneHandlers {
  getMinWidth?: () => number;
  getMaxWidth?: () => number;
  getMinHeight?: () => number;
  getMaxHeight?: () => number;
  getInitialWidth?: () => number;
  getInitialHeight?: () => number;
}
Enter fullscreen mode Exit fullscreen mode

After (v3):

interface SplitPaneHandlers {
  getMinWidth?: () => number | undefined;
  getMaxWidth?: () => number | undefined;
  getMinHeight?: () => number | undefined;
  getMaxHeight?: () => number | undefined;
  getInitialWidth?: () => number | string | undefined;
  getInitialHeight?: () => number | string | undefined;
}
Enter fullscreen mode Exit fullscreen mode

Why: In v2, calling getMinWidth() on a pane with no minWidth prop would return undefined at runtime, but the type said number. The signatures now match the actual behavior. If your code relied on the return type always being number, add a nullish check.

Migration:

// Before -- worked at runtime but was type-unsafe
const min = ref.current.getMinWidth(); // number
const width = min * 2;

// After -- explicit check
const min = ref.current.getMinWidth(); // number | undefined
const width = min !== undefined ? min * 2 : 0;
Enter fullscreen mode Exit fullscreen mode

Size Props Accept ResponsiveValue<T>

The TypeScript types for size, knobSize, and spacing on both Split and Split.Resizer, and all six size props on Split.Pane, have been widened to accept ResponsiveValue<T>. This is backward-compatible at runtime (plain values still work), but if you have wrapper components with strict prop typing, you may need to update their type definitions.

Before:

size?: MantineSize | number | (string & {});
spacing?: MantineSpacing;
Enter fullscreen mode Exit fullscreen mode

After:

size?: ResponsiveValue<MantineSize | number | (string & {})>;
spacing?: ResponsiveValue<MantineSpacing>;
Enter fullscreen mode Exit fullscreen mode

Removed spacing Override in SplitContext

The spacing property was previously defined twice: once in SplitResizerContextProps and again as a separate field in SplitContext. The duplicate has been removed from the context interface. Since SplitContext extends SplitResizerContextProps, the spacing field is still present via inheritance. This only affects you if you were consuming SplitContext directly in custom code -- standard usage through <Split> and <Split.Resizer> is unaffected.

Error Handling in Resize Handlers

The resizer no longer throws an error when adjacent pane refs are unavailable during handleMove, handleMouseUp, or handleTouchEnd. Instead, it returns early. This prevents crashes in edge cases (e.g., when panes are conditionally removed during a drag) but means that if you were catching those errors deliberately, you should switch to defensive checks.

Improvements

Comprehensive JSDoc Documentation

Every public interface, exported type, internal function, and imperative handler now carries English-language JSDoc comments. This improves the developer experience in IDEs -- hover over any prop, handler, or type and you will see a clear description of what it does, what it accepts, and why it exists.

Cleaner getSizeInPixel Implementation

The internal function that converts size values (numbers, "px" strings, percentage strings) to pixels has been simplified and now returns undefined instead of silently returning undefined through a missing code path. The parentElement access is also guarded against null, preventing potential runtime errors in edge cases.

Reset Behavior Recalculates from Current Percentage

Double-clicking a resizer to reset adjacent panes now recalculates the initial size from the current percentage value rather than restoring a stale pixel snapshot. If your container has resized since mount, the reset lands on the correct proportional size.

Storybook Variant List Updated

The gradient variant was missing from the Storybook controls. It is now included alongside default, filled, outline, transparent, dotted, and dashed.

Cleaned Up Storybook Labels

Display labels in the resizer-events story have been corrected from beforePanePane 1 / afterPanePane 1 to beforePane / afterPane.

Migration Guide

For the vast majority of projects, upgrading is a drop-in replacement:

npm install @gfazioli/mantine-split-pane@^3.0.0
Enter fullscreen mode Exit fullscreen mode

Then review these steps:

  1. Check imperative ref usage. If you call getMinWidth(), getMaxWidth(), getMinHeight(), getMaxHeight(), getInitialWidth(), or getInitialHeight() on a pane ref, ensure your code handles undefined return values.

  2. Check wrapper component types. If you have wrapper components that re-export size, knobSize, spacing, or pane size props with strict types, widen them to ResponsiveValue<...> or import the type directly:

   import type { ResponsiveValue } from '@gfazioli/mantine-split-pane';
Enter fullscreen mode Exit fullscreen mode
  1. Check error boundary reliance. If you were catching errors thrown by the resizer during edge-case drag operations, note that those code paths now return early instead of throwing.

  2. No CSS or Styles API changes. All CSS variables, class names, and Styles API selectors remain the same.

Links

Top comments (0)