Mantine Window

Logo

@gfazioli/mantine-window

A Mantine extension component that renders draggable, resizable floating windows with persistent state, customizable boundaries, collapsible content, z-index management, and flexible control over position, size, and interaction modes. Includes a WindowGroup compound component for coordinated multi-window management with layout presets.

Migrating from v1.x

Breaking changes:

v2.0 replaces the object-based defaultPosition and defaultSize props with a flat API.

  • defaultPosition={{ x, y }}defaultX and defaultY
  • defaultSize={{ width, height }}defaultWidth and defaultHeight

Before (v1.x):

<Window
  defaultPosition={{ x: 100, y: 50 }}
  defaultSize={{ width: 400, height: 300 }}
/>

After (v2.0):

<Window
  defaultX={100}
  defaultY={50}
  defaultWidth={400}
  defaultHeight={300}
/>

New features:

  • Controlled position and size — new x, y, width, height props for full external control
  • Responsive values — all dimension props accept Mantine breakpoint objects ({ base: 280, sm: 350, md: 450 })
  • Responsive radius and shadow — these props now accept breakpoint objects too
  • Window.Group — new compound component for coordinated multi-window management with layout presets
  • Tools button — layout menu in the window header with snap, tile, columns, rows, and fill actions
  • withToolsButton prop — control tools button visibility per window or per group (showToolsButton)
  • groupRef — imperative API to apply layouts programmatically
  • Accessibilityrole="dialog", aria-label, and labeled buttons for close, collapse, and tools

Installation

yarn add @gfazioli/mantine-window

After installation import package styles at the root of your application:

import '@gfazioli/mantine-window/styles.css';

You can import styles within a layer @layer mantine-window by importing @gfazioli/mantine-window/styles.layer.css file.

import '@gfazioli/mantine-window/styles.layer.css';

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 Window component within a portal to avoid layout issues. However, if you want the window to be constrained within a specific container, you can set the withinPortal prop to false and the container should have position: relative style.

For example, in the demos below, the Window component is rendered relative to its parent container which has position: relative style.

Color
Shadow
Radius
import { Window, type WindowProps } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';
function Demo() {
  return (
    {/** In this demo, the Window is positioned relative to its parent container. Check the docs below to learn more about the "withinPortal" prop. */}
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        defaultX={0} defaultY={0}
        defaultWidth={320} defaultHeight={256}
        withinPortal={false}
        maxWidth={500}
        maxHeight={500}
        opened
        title="Hello, World! Hello, Mantiners! Say Hello to the Window component"
      >
        <Title order={4}>This is a window with data</Title>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Button, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [opened, { open, close }] = useDisclosure(false);

  return (
    <Stack pos="relative" style={{ width: '100%', height: 500 }}>
      <Button onClick={open}>Open window</Button>
      <Window title="Hello World" opened={opened} onClose={close} withinPortal={false}>
        <Title order={4}>This is a window</Title>
      </Window>
    </Stack>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Custom Position & Size"
        opened
        defaultX={150}
        defaultY={80}
        defaultWidth={350}
        defaultHeight={250}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Position: x=150, y=80</Title>
        <p>Size: 350 x 250</p>
      </Window>
    </Box>
  );
}

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

import { useState } from 'react';
import { Window } from '@gfazioli/mantine-window';
import { Box, Slider, Stack, Text, Title } from '@mantine/core';

function Demo() {
  const [x, setX] = useState(50);
  const [y, setY] = useState(50);
  const [width, setWidth] = useState(300);

  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Stack gap="xs" style={{ position: 'absolute', bottom: 10, left: 10, right: 10, zIndex: 100 }}>
        <Text size="sm">X: {x}</Text>
        <Slider value={x} onChange={setX} min={0} max={400} />
        <Text size="sm">Y: {y}</Text>
        <Slider value={y} onChange={setY} min={0} max={300} />
        <Text size="sm">Width: {width}</Text>
        <Slider value={width} onChange={setWidth} min={200} max={500} />
      </Stack>
      <Window
        title="Controlled"
        opened
        x={x}
        y={y}
        width={width}
        defaultHeight={180}
        onPositionChange={(pos) => { setX(pos.x); setY(pos.y); }}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Drag me or use sliders</Title>
      </Window>
    </Box>
  );
}

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

import { Window } from '@gfazioli/mantine-window';
import { Button, Group, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [portalOpened, { open: openPortal, close: closePortal }] = useDisclosure(false);
  const [containerOpened, { open: openContainer, close: closeContainer }] = useDisclosure(false);

  return (
    <>
      <Group mb="md">
        <Button onClick={openPortal}>Open in Portal (Fixed)</Button>
        <Button onClick={openContainer}>Open in Container (Relative)</Button>
      </Group>

      {/* Window with withinPortal={true} (default) - fixed positioning */}
      <Window 
        title="Fixed Window (Portal)" 
        opened={portalOpened} 
        onClose={closePortal}
        withinPortal={true}
        persistState={false}
        defaultX={100} defaultY={100}
      >
        <Stack p="md">
          <Title order={4}>Portal Window</Title>
          <Text size="sm">
            This window uses <strong>withinPortal=true</strong> (default).
            It is positioned fixed relative to the viewport and will stay in place even when scrolling.
          </Text>
        </Stack>
      </Window>

      {/* Container for relative window */}
      <Stack pos="relative" style={{ width: '100%', height: 400, border: '2px dashed gray' }} p="md">
        <Text c="dimmed" size="sm">Container with position: relative</Text>

        {/* Window with withinPortal={false} - absolute positioning */}
        <Window
          title="Relative Window (Container)"
          opened={containerOpened}
          onClose={closeContainer}
          withinPortal={false}
          persistState={false}
          defaultX={0} defaultY={0}
          defaultWidth={400} defaultHeight={300}
        >
          <Stack p="md">
            <Title order={4}>Container Window</Title>
            <Text size="sm">
              This window uses <strong>withinPortal=false</strong>.
              It is positioned absolute relative to its parent container and cannot be dragged or resized outside the container boundaries.
            </Text>
          </Stack>
        </Window>
      </Stack>
    </>
  );
}

Unit Types

Position and size props support three unit types:

  • Pixels (number): Fixed pixel values, e.g. defaultX={100}
  • Viewport units (string): vw for viewport width, vh for 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 of withinPortal setting
  • %: With withinPortal={true} (default) relative to the viewport; with withinPortal={false} relative to the parent container
  • Pixels: Always fixed, regardless of withinPortal setting

You can mix different unit types freely on the same component.

import { Window } from '@gfazioli/mantine-window';
import { Box, Stack, Text, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500, border: '2px dashed gray' }}>
      {/* Pixels */}
      <Window
        title="Pixels"
        opened
        defaultX={10}
        defaultY={10}
        defaultWidth={200}
        defaultHeight={180}
        persistState={false}
        withinPortal={false}
      >
        <Stack gap="xs">
          <Text size="sm"><strong>defaultX:</strong> 10</Text>
          <Text size="sm"><strong>defaultWidth:</strong> 200</Text>
        </Stack>
      </Window>

      {/* Percentages */}
      <Window
        title="Percentages"
        opened
        defaultX="40%"
        defaultY={10}
        defaultWidth={200}
        defaultHeight={180}
        persistState={false}
        withinPortal={false}
      >
        <Stack gap="xs">
          <Text size="sm"><strong>defaultX:</strong> "40%"</Text>
          <Text size="sm">Relative to container</Text>
        </Stack>
      </Window>

      {/* Percentages */}
      <Window
        title="Mixed"
        opened
        defaultX={10}
        defaultY="55%"
        defaultWidth="60%"
        defaultHeight={150}
        persistState={false}
        withinPortal={false}
      >
        <Stack gap="xs">
          <Text size="sm"><strong>defaultY:</strong> "55%"</Text>
          <Text size="sm"><strong>defaultWidth:</strong> "60%"</Text>
          <Text size="sm">Mix px, vw/vh, and % freely</Text>
        </Stack>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Stack, Text, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Responsive Window"
        opened
        defaultX={{ base: 10, md: 50 }}
        defaultY={20}
        defaultWidth={{ base: 280, sm: 350, md: 450 }}
        defaultHeight={{ base: 200, md: 300 }}
        minWidth={{ base: 200, md: 300 }}
        radius={{ base: 'sm', md: 'lg' }}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Responsive Props</Title>
        <Stack gap="xs">
          <Text size="sm">
            <strong>defaultWidth:</strong>{' '}
            {'{ base: 280, sm: 350, md: 450 }'}
          </Text>
          <Text size="sm">
            <strong>defaultHeight:</strong>{' '}
            {'{ base: 200, md: 300 }'}
          </Text>
          <Text size="sm">
            <strong>minWidth:</strong>{' '}
            {'{ base: 200, md: 300 }'}
          </Text>
          <Text size="sm">
            <strong>radius:</strong>{' '}
            {'{ base: "sm", md: "lg" }'}
          </Text>
          <Text size="sm" c="dimmed" mt="xs">
            Resize your browser to see values change at breakpoints.
          </Text>
        </Stack>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Constrained Resize"
        opened
        defaultX={50} defaultY={50}
        defaultWidth={400} defaultHeight={300}
        minWidth={300}
        minHeight={200}
        maxWidth={600}
        maxHeight={450}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Try resizing this window</Title>
        <p>Min: 300x200px</p>
        <p>Max: 600x450px</p>
      </Window>
    </Box>
  );
}

You can mix different unit types for constraints — for example, a fixed pixel minimum with a viewport-based maximum:

import { Window } from '@gfazioli/mantine-window';
import { Button, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [opened, { open, close }] = useDisclosure(false);

  return (
    <>
      <Button onClick={open}>Open window</Button>
      <Window
        title="Mixed Unit Constraints"
        opened={opened}
        onClose={close}
        defaultX={50} defaultY={50}
        defaultWidth={400} defaultHeight={350}
        minWidth={300}
        minHeight="15vh"
        maxWidth="60vw"
        maxHeight={600}
        persistState={false}
      >
        <Title order={4}>Mixed Units</Title>
        <Stack gap="sm">
          <p>
            <strong>Min Width:</strong> 300px (fixed)
          </p>
          <p>
            <strong>Min Height:</strong> 15vh (viewport-based)
          </p>
          <p>
            <strong>Max Width:</strong> 60vw (viewport-based)
          </p>
          <p>
            <strong>Max Height:</strong> 600px (fixed)
          </p>
        </Stack>
      </Window>
    </>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Bounded Dragging"
        opened
        defaultX={100} defaultY={50}
        defaultWidth={400} defaultHeight={300}
        dragBounds={{
          minX: 50,
          maxX: 500,
          minY: 50,
          maxY: 400,
        }}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Try dragging this window</Title>
        <p>It can only move within specific bounds:</p>
        <p>X: 50-500, Y: 50-400</p>
      </Window>
    </Box>
  );
}

Combine different unit types (pixels, viewport units, percentages) for drag boundaries to create flexible constraints:

import { Window } from '@gfazioli/mantine-window';
import { Button, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [opened, { open, close }] = useDisclosure(false);

  return (
    <>
      <Button onClick={open}>Open window</Button>
      <Window
        title="Mixed Unit Drag Bounds"
        opened={opened}
        onClose={close}
        defaultX={50} defaultY="10vh"
        defaultWidth={400} defaultHeight={300}
        dragBounds={{
          minX: 50,
          maxX: '80vw',
          minY: '10vh',
          maxY: 450,
        }}
        persistState={false}
      >
        <Title order={4}>Mixed Unit Boundaries</Title>
        <Stack gap="sm">
          <p>
            <strong>Min X:</strong> 50px (fixed)
          </p>
          <p>
            <strong>Max X:</strong> 80vw (viewport-based)
          </p>
          <p>
            <strong>Min Y:</strong> 10vh (viewport-based)
          </p>
          <p>
            <strong>Max Y:</strong> 450px (fixed)
          </p>
          <p>Mix different unit types for flexible boundaries!</p>
        </Stack>
      </Window>
    </>
  );
}

Centered Window

Create a centered, fixed window that cannot be moved or resized by combining position/size props with resizable="none" and draggable="none".

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Centered Window"
        opened
        defaultX={190} defaultY={100}
        defaultWidth={420} defaultHeight={300}
        resizable="none"
        draggable="none"
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Centered, fixed window</Title>
        <p>This window is centered and cannot be moved or resized</p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="No Collapsable"
        opened
        defaultX={50} defaultY={50}
        defaultWidth={400} defaultHeight={300}
        collapsable={false}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>This window cannot be collapsed</Title>
        <p>Double-clicking the header will not collapse the window</p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Full Size Resize Handles"
        opened
        defaultX={50} defaultY={50}
        defaultWidth={450} defaultHeight={350}
        fullSizeResizeHandles
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Full Size Resize Handles</Title>
        <p>
          With <code>fullSizeResizeHandles</code> enabled, the side handles (top, bottom, left,
          right) span the entire width or height of the window.
        </p>
        <p>
          This makes it easier to resize from any edge, while corner handles (14x14px) remain for
          diagonal resizing.
        </p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="With Persistence"
        id="persistent-window-demo"
        opened
        defaultX={50} defaultY={50}
        defaultWidth={400} defaultHeight={300}
        persistState
        withinPortal={false}
      >
        <Title order={4}>Position, size, and collapsed state are saved in localStorage</Title>
        <p>Move, resize, or collapse/expand this window, then refresh the page</p>
        <p>The window will remember its last position, size, and collapsed state</p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="With Callbacks"
        opened
        defaultX={50} defaultY={50}
        defaultWidth={400} defaultHeight={300}
        persistState={false}
        withinPortal={false}
        onPositionChange={(pos) => {
          // eslint-disable-next-line no-console
          console.log('Position changed:', pos);
        }}
        onSizeChange={(size) => {
          // eslint-disable-next-line no-console
          console.log('Size changed:', size);
        }}
      >
        <Title order={4}>Open console to see callbacks</Title>
        <p>Move or resize this window to trigger callbacks</p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Box, Title } from '@mantine/core';

function Demo() {
  return (
    <Box pos="relative" style={{ width: '100%', height: 500 }}>
      <Window
        title="Window 1"
        id="demo-window-1"
        opened
        defaultX={20} defaultY={20}
        defaultWidth={300} defaultHeight={250}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>First Window</Title>
        <p>Click on a window to bring it to front</p>
      </Window>
      <Window
        title="Window 2"
        id="demo-window-2"
        opened
        defaultX={180} defaultY={80}
        defaultWidth={300} defaultHeight={250}
        persistState={false}
        withinPortal={false}
      >
        <Title order={4}>Second Window</Title>
        <p>Click on a window to bring it to front</p>
      </Window>
    </Box>
  );
}

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.

import { Window } from '@gfazioli/mantine-window';
import { Title } from '@mantine/core';

function Demo() {
  return (
    <Window.Group style={{ width: '100%', height: 500 }}>
      <Window
        id="editor"
        title="Editor"
        opened
        defaultX={10}
        defaultY={10}
        defaultWidth={300}
        defaultHeight={250}
      >
        <Title order={4}>Editor window</Title>
        <p>Try the green button for layout options</p>
      </Window>
      <Window
        id="preview"
        title="Preview"
        opened
        defaultX={320}
        defaultY={10}
        defaultWidth={300}
        defaultHeight={250}
      >
        <Title order={4}>Preview window</Title>
      </Window>
      <Window
        id="console"
        title="Console"
        opened
        defaultX={10}
        defaultY={270}
        defaultWidth={610}
        defaultHeight={200}
      >
        <Title order={4}>Console window</Title>
      </Window>
    </Window.Group>
  );
}

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 with collapsable={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:

none
import { useState } from 'react';
import { Window, type WindowLayout } from '@gfazioli/mantine-window';
import { Badge, Group, Stack, Text, Title } from '@mantine/core';

function Demo() {
  const [lastLayout, setLastLayout] = useState<WindowLayout | null>(null);

  return (
    <Stack gap="sm">
      <Group>
        <Text size="sm">Last layout applied:</Text>
        <Badge>{lastLayout ?? 'none'}</Badge>
      </Group>
      <Window.Group
        style={{ width: '100%', height: 500 }}
        defaultLayout="tile"
        onLayoutChange={setLastLayout}
      >
        <Window id="w1" title="Window 1" opened defaultX={10} defaultY={10} defaultWidth={250} defaultHeight={200}>
          <Title order={4}>Window 1</Title>
          <p>Use the green button to apply layouts</p>
        </Window>
        <Window id="w2" title="Window 2" opened defaultX={270} defaultY={10} defaultWidth={250} defaultHeight={200}>
          <Title order={4}>Window 2</Title>
        </Window>
        <Window id="w3" title="Window 3" opened defaultX={10} defaultY={220} defaultWidth={250} defaultHeight={200}>
          <Title order={4}>Window 3</Title>
        </Window>
        <Window id="w4" title="Window 4" opened defaultX={270} defaultY={220} defaultWidth={250} defaultHeight={200}>
          <Title order={4}>Window 4</Title>
        </Window>
      </Window.Group>
    </Stack>
  );
}

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.

import { useRef, useState } from 'react';
import { Window, type WindowGroupContextValue, type WindowLayout } from '@gfazioli/mantine-window';
import { Select, Stack, Title } from '@mantine/core';

const layoutOptions = [
  { value: 'tile', label: 'Tile' },
  { value: 'arrange-columns', label: 'Columns' },
  { value: 'arrange-rows', label: 'Rows' },
];

function Demo() {
  const [layout, setLayout] = useState<string | null>('tile');
  const groupRef = useRef<WindowGroupContextValue>(null);

  return (
    <Stack gap="sm">
      <Select
        label="Layout"
        data={layoutOptions}
        value={layout}
        onChange={(value) => {
          setLayout(value);
          if (value) {
            groupRef.current?.applyLayout(value as WindowLayout);
          }
        }}
        w={200}
      />
      <Window.Group
        groupRef={groupRef}
        style={{ width: '100%', height: 450 }}
        defaultLayout="tile"
        onLayoutChange={(l) => setLayout(l)}
      >
        <Window id="lp-1" title="Window 1" opened>
          <Title order={4}>Window 1</Title>
        </Window>
        <Window id="lp-2" title="Window 2" opened>
          <Title order={4}>Window 2</Title>
        </Window>
        <Window id="lp-3" title="Window 3" opened>
          <Title order={4}>Window 3</Title>
        </Window>
      </Window.Group>
    </Stack>
  );
}

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

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */