@gfazioli/mantine-window
A Mantine extension that renders draggable, resizable floating windows with keyboard resizing, persistent state, drag bounds, an axis lock, collapsible content and z-index management. Includes a WindowGroup compound component with layout presets, and a headless useDragResize hook that brings the same drag and resize to any element.
Upgrading from an older major version? See the Upgrade guide for breaking changes and step-by-step migration instructions.
Installation
After installation import package styles at the root of your application:
You can import styles within a layer @layer mantine-window by importing @gfazioli/mantine-window/styles.layer.css file.
Comparison with Mantine core FloatingWindow
Since version 9.0, Mantine core ships a first-party FloatingWindow component, built on the headless use-floating-window hook. The basics overlap — a draggable, resizable surface with size limits and gesture callbacks — but the two make different trade-offs.
When to use which. Reach for the core FloatingWindow for a light draggable panel you style yourself — a floating toolbar, a chat box, a picture-in-picture player — when you would rather not add a dependency. Reach for Window when you want desktop-style windows: a title bar with its controls, resizing from any edge, sizes in viewport units or percentages that follow the breakpoints, a window confined to a container, position and size kept across reloads with persistState, and several windows arranged together with Window.Group. Reach for its headless hook when you want that same geometry on an element of your own.
Usage
The Window component allows you to create draggable and resizable windows within your application. It provides a familiar desktop-like experience for users. You can customize the appearance and behavior of the windows using various props.
Note:
Usually, you would want to use the
Windowcomponent within a portal to avoid layout issues. However, if you want the window to be constrained within a specific container, you can set thewithinPortalprop tofalseand the container should haveposition: relativestyle.For example, in the demos below, the
Windowcomponent is rendered relative to its parent container which hasposition: relativestyle.
Hello, World! Hello, Mantiners! Say Hello to the Window component
Controlled
You can control the open and close state of the Window component using the opened and onClose props. This allows you to manage the visibility of the window programmatically.
Position and Size
Set the initial position and size of the window using defaultX, defaultY, defaultWidth, and defaultHeight props. These values are used when the window is first rendered and can be modified by the user via drag and resize.
Custom Position & Size
Controlled Position and Size
For full external control, use the x, y, width, and height props. When these are set, the component does not manage that value internally — your state drives it. Combine with onPositionChange and onSizeChange to sync state after user interactions.
You can also mix controlled and uncontrolled: for example, control only x and y while letting width and height remain uncontrolled via defaultWidth and defaultHeight.
X: 50
Y: 50
Width: 300
Controlled
Within Portal vs Container
The withinPortal prop controls how the window is positioned. When withinPortal={true} (default), the window uses fixed positioning relative to the viewport. When withinPortal={false}, it uses absolute positioning relative to its parent container.
Fixed positioning (withinPortal=true):
- Window stays in place when scrolling
- Positioned relative to the browser viewport
- Best for modals and overlays
Absolute positioning (withinPortal=false):
- Window is constrained within parent container
- Parent must have
position: relative - Window cannot be dragged or resized outside container boundaries
- Best for embedded UI components
Container with position: relative
Unit Types
Position and size props support three unit types:
- Pixels (number): Fixed pixel values, e.g.
defaultX={100} - Viewport units (string):
vwfor viewport width,vhfor viewport height, e.g.defaultX="10vw" - Percentages (string): Relative to the reference container, e.g.
defaultX="20%"
Understanding Unit References:
vw/vh: Always relative to the browser viewport, regardless ofwithinPortalsetting%: WithwithinPortal={true}(default) relative to the viewport; withwithinPortal={false}relative to the parent container- Pixels: Always fixed, regardless of
withinPortalsetting
You can mix different unit types freely on the same component.
Pixels
Percentages
Mixed
Responsive
All dimension and position props support responsive values using Mantine breakpoints. Pass an object with breakpoint keys (base, xs, sm, md, lg, xl) to change values at different screen sizes.
This also applies to minWidth, maxWidth, minHeight, maxHeight, radius, and shadow.
Responsive Window
Min/Max Size Constraints
Control the minimum and maximum dimensions during resize operations using minWidth, minHeight, maxWidth, and maxHeight props. These support all unit types (pixels, viewport units, percentages) and responsive breakpoint objects.
Constrained Resize
You can mix different unit types for constraints — for example, a fixed pixel minimum with a viewport-based maximum:
Drag Boundaries
Restrict the draggable area of the window using the dragBounds prop. This ensures the window stays within specific boundaries. The dragBounds object accepts minX, maxX, minY, and maxY values, each supporting pixels, viewport units, and percentages.
Bounded Dragging
Combine different unit types (pixels, viewport units, percentages) for drag boundaries to create flexible constraints:
Axis Lock
Set axis="x" or axis="y" to restrict dragging to one direction: a window that slides along a dock, a panel that only moves up and down. The locked coordinate stays where the gesture started, while the free one is still kept inside the viewport, the container or dragBounds.
Only the user's drag is locked. A controlled x / y, the layouts of the tools menu and of Window.Group still move the window anywhere.
axis="x"
axis="y"
Touch Screens and Small Boundaries
On a phone or a tablet, a window behaves as a finger expects:
- A swipe on content that overflows scrolls it, because a finger has no wheel. A swipe on the title bar, or on content that fits, drags the window as before. The mouse is unchanged: it still drags from anywhere and scrolls with the wheel.
- Each window follows only the finger that pressed it, so a second finger resting on another window does not drag that one too.
- The resize handles and the title-bar buttons get bigger hit areas on a coarse pointer, and the buttons show their glyphs, since there is no hover to reveal them.
keepInBounds (on by default) keeps every window reachable inside its boundary, the viewport or its container:
- When the boundary is measured, and whenever it or the window changes size (a phone rotating, a browser window or a container getting narrower, a collapsed window opening), a window that sticks out moves back inside it, and one larger than the boundary shrinks to fit. A collapsed window keeps its expanded height until it opens.
dragBoundscan only narrow the boundary: bounds written for a wide screen no longer let a window out of a phone's.- Resizing with the pointer stops at the boundary, the viewport included, as keyboard resizing does.
Set keepInBounds={false} for the 3.3 behavior, for example to let a window hide partly off-screen through dragBounds. Moves made from code (a controlled x / y, the layouts) are applied as given: only a later change in the size of the boundary or of the window fits them again.
Container width: 100%
Shrink my container
Centered Window
Create a centered, fixed window that cannot be moved or resized by combining position/size props with resizable="none" and draggable="none".
Centered Window
Disable Collapsing
Prevent the window from being collapsed by setting collapsable={false}. This removes the collapse functionality, including the double-click handler on the header.
No Collapsable
Disable ScrollArea
By default, the window content is wrapped in a ScrollArea component. Set withScrollArea={false} to disable it — content that overflows will be clipped instead of scrollable. This is useful when your content already provides its own scrolling mechanism or when the ScrollArea interferes with the inner layout.
With ScrollArea (default)
Without ScrollArea
Controls Position
The window title bar buttons (close, collapse, tools) default to the left side (macOS style). Set controlsPosition="right" to move them to the right side (Windows style). The button order is automatically reversed when positioned on the right — close moves to the far right, matching platform conventions.
For full control over button ordering, use the controlsOrder prop with an array of 'close', 'collapse', and 'tools' in your desired sequence. When controlsOrder is set, it overrides the automatic ordering.
macOS style (default)
Windows style
Custom order
Full Size Resize Handles
By default, resize handles (top, bottom, left, right) are centered and have a limited size (40px). Setting fullSizeResizeHandles={true} makes these handles span the entire width or height of the window edge, providing a larger interaction area.
Full Size Resize Handles
Keyboard Resizing
Every resizable window has one resize handle that takes keyboard focus: the bottom-right corner, or the right edge when the window is collapsed or resizable="horizontal", or the bottom edge with resizable="vertical". One tab stop per window: the other seven handles stay pointer-only. With the handle focused:
- the arrow keys move its edge by
resizeSteppixels (default10), and Shift + arrows byresizeShiftStep(default50); - Home takes the window to its min size, and End to its max size.
No key ever takes the edge past the viewport or the container: there the edge wins over the min and max sizes, as it does when you resize with the pointer. End without a max size therefore stops at that edge.
The arrows are physical, as in the WAI-ARIA window splitter pattern: the window is placed with left and top, so in a right-to-left layout too the right arrow moves the right edge to the right.
The handle is a WAI-ARIA separator. Screen readers announce its name, resizeHandleLabel (default 'Resize window'), and its value: the width, or the height on the bottom edge, between aria-valuemin and aria-valuemax. On the corner, which changes both, aria-valuetext reads the width × height. While it has keyboard focus the handle is drawn in the primary color and the window is outlined.
Each key press is a complete resize gesture: it brings the window to the front, fires onResizeStart, onSizeChange and onResizeEnd like a pointer resize, and is saved by persistState. Set withKeyboardResize={false} to keep every handle pointer-only.
Resize me from the keyboard
Persistence
Enable state persistence to save the window's position and size in localStorage by setting persistState={true} (default is false). When enabled, the window will remember its position and size across page refreshes.
With Persistence
Callbacks
Use onPositionChange and onSizeChange callbacks to respond to window movements and resize operations. These callbacks receive the current position or size as pixel values.
With Callbacks
Drag and Resize Lifecycle
onPositionChange and onSizeChange fire continuously, on every frame of a gesture. Four more callbacks mark its edges instead: onDragStart, onDragEnd, onResizeStart and onResizeEnd. They mirror the set that Mantine core's FloatingWindow exposes.
The edges are what make a gesture composable. Persist the layout once, on end, instead of debouncing every frame by hand; pause an expensive child, an iframe or a video while the user drags and resume after; show a snap guide or a dimension badge only while the gesture runs; record a single undo entry per gesture rather than one per pixel.
Each start is paired with exactly one end. A press on a resize handle, on an interactive child (input, button, link, …) or on a data-no-window-drag region does not open a drag, so no onDragStart fires there either — and a resize never emits drag callbacks, nor the other way round. Unmounting mid-gesture still fires the matching end, so a paused child is never left paused.
Drag or resize me
Multiple Windows
You can render multiple windows in the same container. Each window maintains its own z-index and can be brought to the front by clicking on it. Use unique id props to ensure proper state persistence.
Window 1
Window 2
Context Menu
Mantine 9.3 introduces the Menu.ContextMenu component: attach desktop-style right-click actions to any surface. Inside a Window it completes the desktop metaphor — and in this demo the actions are real: duplicate the window or close it, with the windows rendered dynamically from state.
Window 1
Window Menu Bar
Mantine 9.4 ships a real Menubar component — a horizontal row of menus with full keyboard
navigation (arrow keys move between top-level menus, and once one is open, moving to a sibling
opens it immediately) and the WAI-ARIA menubar semantics. Dropped into a Window it completes the
desktop-application metaphor. In this demo every entry drives the workspace for real: the File
and Edit menus report the executed command in the status bar, while the View menu toggles the
canvas grid and changes the layout density. Because the Window drag handler ignores interactive
targets — and the menu triggers carry the menuitem role — the menu bar stays fully usable
inside the draggable surface.
Design Workspace
Form Controls
A Window is a regular content surface: any interactive Mantine input works inside it and
keeps its native behavior. In particular a searchable Select can be focused, typed into
and filtered — the window only starts dragging when you grab a non-interactive area (or the
header), so form fields never steal the drag and the drag never steals their focus. You can
opt any custom region out of dragging with the data-no-window-drag attribute.
Profile settings
Window.Group
Wrap multiple windows in a Window.Group to enable coordinated window management. The group provides:
- Coordinated z-index: Windows within a group share a stacking context
- Layout presets: Snap, tile, and fill layouts via the green tools button in each window header
- Global actions: Collapse all, expand all, close all
Each Window inside a Window.Group must have a unique id prop. The group defaults to withinPortal={false} (overridable via the withinPortal prop) and applies position: relative on its container.
Editor
Preview
Console
Tools Button
The green tools button in the window header provides layout options. It is controlled by the withToolsButton prop (default: true) and is available on every window, even outside a Window.Group.
Without a Group, the menu shows only single-window actions:
- Move & Resize: Snap left, snap right, snap top, snap bottom — positions the current window to fill half of the container or viewport
- Fill: Maximizes the current window to fill the entire available area
Inside a Group, the menu also shows group-wide actions:
- Arrange Columns / Rows: Distributes all visible windows in equal columns or rows
- Tile: Arranges all visible windows in an automatic grid
- Collapse all / Expand all: Collapses or expands all windows that have
collapsable={true}. Windows withcollapsable={false}are skipped. - Close all: Closes all windows in the group
The onLayoutChange callback on Window.Group notifies you when a group layout is applied.
Last layout applied:
Window 1
Window 2
Window 3
Window 4
Layout Presets
Use defaultLayout to apply an initial layout when the group mounts. To control the layout externally, use the groupRef prop to access the group API and call applyLayout programmatically.
Window 1
Window 2
Window 3
Dynamic Windows
Windows inside a Window.Group can be rendered dynamically from an array — for example via .map() over a piece of state. Layout presets and group actions wait until the registry is populated and the container is measured, so a layout triggered before all children have mounted is automatically deferred and flushed when everything is ready.
Window alpha
Window beta
Z-index Management
Both Window.Group and stand-alone Window expose z-index configuration via three props:
initialZIndex— starting value for the stacking context. Defaults to200whenwithinPortalistrue,1otherwise.maxZIndex— upper bound. When the counter would exceed this value it wraps back toinitialZIndex, preventing unbounded escalation above modals or menus.zIndexStrategy(Window.Grouponly) — how z-indexes are assigned when a window is focused or registered:'increment'(default): a counter monotonically increases. Preserves legacy behavior. Combine withmaxZIndexto keep values bounded.'normalize': z-indexes are derived from the stacking order and always stay withininitialZIndex .. initialZIndex + N - 1. Recommended for long-running applications or when Mantine modals and menus are used alongside.
Click each window in any order. With normalize, z-indexes always stay compact ({100..102}). With increment, the counter keeps growing unless maxZIndex is set.
Alpha
Beta
Gamma
Headless Hook
useDragResize is the engine Window is built on, exported so you can put the same geometry on any element: a card, a sticky note, an image, a floating toolbar. Core's use-floating-window drags an element; this hook also resizes it, from any edge or corner.
The hook computes, and you render: put position on left / top and size on width / height, with position: fixed for boundary: 'viewport' (the default) or position: absolute in a positioned parent for boundary: 'parent'.
Options. Everything is optional.
position/defaultPositionandsize/defaultSize: controlled or uncontrolled, in pixels,vw,vhor a percentage of the boundary. The defaults are those ofWindow:{ x: 20, y: 100 }and400 × 400.minWidth,maxWidth,minHeight,maxHeight: the same units. The mins default to250and100, likeWindow's.boundary:'viewport'or'parent'. The element is kept inside it while dragging, and a parent also limits resizing.dragBounds: a narrower rectangle for the position, in the same units.axis:'x'or'y', to lock dragging to one direction.keepInBounds: keeps the element inside the boundary. It moves back inside, and shrinks to fit, when the boundary is measured or changes size;dragBoundscan only narrow the boundary; a pointer resize stops at the viewport edge.trueby default.resizeStepandresizeShiftStep: the keyboard steps,10and50pixels by default.onPositionChangeandonSizeChange, with pixel values, andonDragStart,onDragEnd,onResizeStartandonResizeEnd, at the edges of each gesture.
Returns.
ref: for the element that moves.positionandsize: the current values, in pixels.setPositionandsetSize: move or resize it from code, in pixels.isDraggingandisResizing:truewhile a pointer gesture runs.boundarySize: the size of the viewport or of the parent,0until it has been measured.getDragHandleProps(): spread it on the area that starts a drag. A press on an input, a button, a link or adata-no-window-dragregion never starts one, as inWindow.getResizeHandleProps(direction, options): spread it on a resize handle for'top','right','bottom','left'or a corner such as'bottomRight'. Pass{ keyboard: true, label }to one handle to make it the focusableseparatordescribed in Keyboard Resizing.
Persistence, collapsing, stacking, the title bar and Window.Group stay in Window: the hook is only the geometry.
A plain Paper
Drag it anywhere, resize it from the right, the bottom or the corner.
300 × 190 at 40, 40Styles API
Window supports Styles API, you can add styles to any inner element of the component with classNames prop. Follow Styles API documentation to learn more.
Styles API
Component Styles API
Hover over selectors to highlight corresponding elements