Skip to main content
react-md

Window Splitter

The WindowSplitter component can be used with the useWindowSplitter hook to allow the user to resize windows or panels within the application using the mouse or keyboard. The --rmd-window-splitter-position custom property can be used to set the height or width of panels and is used to set the WindowSplitter's position within the viewport or relative container.

If the entire layout should be resizable, see the Resizable Navigation Layout and useResizableLayout instead.

Horizontal Resizable Sheet Example

This example will show how to create a resizable Sheet using the WindowSplitter. The useWindowSplitter hook requires a min and max value (in pixels) and provides the current value (in pixels) which can be used to set the width or update custom properties.

The useWindowSize hook can be used to dynamically set the max value if the user resizes the window.

"use client";

import { Button } from "@react-md/core/button/Button";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Sheet } from "@react-md/core/sheet/Sheet";
import { useToggle } from "@react-md/core/useToggle";
import { useWindowSize } from "@react-md/core/useWindowSize";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement, useId } from "react";

export default function HorizontalResizableSheetExample(): ReactElement {
  const sheetId = useId();
  const titleId = useId();

  const { enable: show, disable: hide, toggled: visible } = useToggle();

  const { width } = useWindowSize({ disableHeight: true });
  const { value, splitterProps } = useWindowSplitter({
    min: 256,
    max: Math.max(400, width - width / 4),
    defaultValue: 256,
  });

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Sheet
        aria-labelledby={titleId}
        id={sheetId}
        visible={visible}
        onRequestClose={hide}
        style={{
          "--rmd-sheet-static-width": `${value}px`,
          "--rmd-window-splitter-position": `${value}px`,
        }}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Resizable Sheet</DialogTitle>
          <Button aria-label="Close" onClick={hide} buttonType="icon">
            <CloseIcon />
          </Button>
        </DialogHeader>
        <WindowSplitter
          {...splitterProps}
          aria-controls={sheetId}
          aria-label="Resize Sheet"
        />
      </Sheet>
    </>
  );
}

Press Enter to start editing.

Reversed Horizontal Resizable Sheet Example

If the panel size should increment when dragging from right-to-left instead of left-to-right, enable the reversed option for the useWindowSplitter hook.

The dragging behavior automatically updates for right-to-left languages when using the WritingDirectionProvider. Try changing the website orientation through the options menu to see it in action.

"use client";

import { Button } from "@react-md/core/button/Button";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Sheet } from "@react-md/core/sheet/Sheet";
import { useToggle } from "@react-md/core/useToggle";
import { useWindowSize } from "@react-md/core/useWindowSize";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement, useId } from "react";

export default function ReversedHorizontalResizableSheetExample(): ReactElement {
  const sheetId = useId();
  const titleId = useId();

  const { enable: show, disable: hide, toggled: visible } = useToggle();

  const { width } = useWindowSize({ disableHeight: true });
  const { value, splitterProps } = useWindowSplitter({
    min: 256,
    max: Math.max(400, width - width / 4),
    reversed: true,
    defaultValue: 256,
  });

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Sheet
        aria-labelledby={titleId}
        id={sheetId}
        visible={visible}
        onRequestClose={hide}
        style={{
          "--rmd-sheet-static-width": `${value}px`,
          "--rmd-window-splitter-position": `${value}px`,
        }}
        position="right"
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Resizable Sheet</DialogTitle>
          <Button aria-label="Close" onClick={hide} buttonType="icon">
            <CloseIcon />
          </Button>
        </DialogHeader>
        <WindowSplitter
          {...splitterProps}
          aria-controls={sheetId}
          aria-label="Resize Sheet"
        />
      </Sheet>
    </>
  );
}

Press Enter to start editing.

Vertical Resizable Sheet Example

The WindowSplitter and useWindowSplitter can be used to resize vertically as well. Just enable the vertical option for the useWindowSplitter.

"use client";

import { Button } from "@react-md/core/button/Button";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Sheet } from "@react-md/core/sheet/Sheet";
import { useToggle } from "@react-md/core/useToggle";
import { useWindowSize } from "@react-md/core/useWindowSize";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement, useId } from "react";

export default function VerticalResizableSheetExample(): ReactElement {
  const sheetId = useId();
  const titleId = useId();

  const { enable: show, disable: hide, toggled: visible } = useToggle();

  const { height } = useWindowSize({ disableWidth: true });
  const { value, splitterProps } = useWindowSplitter({
    min: 256,
    // allow up to 3/4 of the window size
    max: Math.max(400, height - height / 4),

    // this normally defaults to `Math.ceil((max - min) / 2)`
    defaultValue: 256,
    vertical: true,
  });

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Sheet
        aria-labelledby={titleId}
        id={sheetId}
        visible={visible}
        onRequestClose={hide}
        style={{
          "--rmd-sheet-height": `${value}px`,
          "--rmd-window-splitter-position": `${value}px`,
        }}
        position="top"
        verticalSize="touch"
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Resizable Sheet</DialogTitle>
          <Button aria-label="Close" onClick={hide} buttonType="icon">
            <CloseIcon />
          </Button>
        </DialogHeader>
        <WindowSplitter
          {...splitterProps}
          aria-controls={sheetId}
          aria-label="Resize Sheet"
        />
      </Sheet>
    </>
  );
}

Press Enter to start editing.

Reversed Vertical Resizable Sheet Example

If the panel size should increment when dragging from down-to-up instead of up-to-down, enable the reversed option for the useWindowSplitter hook.

"use client";

import { Button } from "@react-md/core/button/Button";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Sheet } from "@react-md/core/sheet/Sheet";
import { useToggle } from "@react-md/core/useToggle";
import { useWindowSize } from "@react-md/core/useWindowSize";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement, useId } from "react";

export default function ReversedVerticalResizableSheetExample(): ReactElement {
  const sheetId = useId();
  const titleId = useId();

  const { enable: show, disable: hide, toggled: visible } = useToggle();

  const { height } = useWindowSize({ disableWidth: true });
  const { value, splitterProps } = useWindowSplitter({
    min: 256,
    // allow up to 3/4 of the window size
    max: Math.max(400, height - height / 4),

    // this normally defaults to `Math.ceil((max - min) / 2)`
    defaultValue: 256,
    vertical: true,
    reversed: true,
  });

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Sheet
        aria-labelledby={titleId}
        id={sheetId}
        visible={visible}
        onRequestClose={hide}
        style={{
          "--rmd-sheet-height": `${value}px`,
          "--rmd-window-splitter-position": `${value}px`,
        }}
        position="bottom"
        verticalSize="touch"
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Resizable Sheet</DialogTitle>
          <Button aria-label="Close" onClick={hide} buttonType="icon">
            <CloseIcon />
          </Button>
        </DialogHeader>
        <WindowSplitter
          {...splitterProps}
          aria-controls={sheetId}
          aria-label="Resize Sheet"
        />
      </Sheet>
    </>
  );
}

Press Enter to start editing.

Relative Horizontal Example

The WindowSplitter defaults to using position: fixed but can also be used within position: relative containers by enabling the disableFixed prop. The max value will also need to be updated to be based on the container size which can be done using the useElementSize hook or another implementation.

Main Panel
Second Panel
"use client";

import { useSsr } from "@react-md/core/SsrProvider";
import { Box } from "@react-md/core/box/Box";
import { Card } from "@react-md/core/card/Card";
import { CardContent } from "@react-md/core/card/CardContent";
import { useElementSize } from "@react-md/core/useElementSize";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import { type ReactElement, useId } from "react";

import styles from "./RelativeHorizontalExample.module.scss";

const MIN_MAIN_PANEL_WIDTH = 140;
const MIN_SECOND_PANEL_WIDTH = 240;

export default function RelativeHorizontalExample(): ReactElement {
  const panelId = useId();
  const ssr = useSsr();
  const { width, elementRef } = useElementSize({
    disableHeight: true,
    defaultValue: () => {
      let width = 1080;
      if (!ssr) {
        width = window.innerWidth;
      }

      return { height: 80, width };
    },
  });
  const max = width - MIN_SECOND_PANEL_WIDTH;
  const { value, splitterProps } = useWindowSplitter({
    min: MIN_MAIN_PANEL_WIDTH,
    max,
  });

  return (
    <Box
      align="stretch"
      grid
      fullWidth
      ref={elementRef}
      style={{
        "--rmd-window-splitter-position": `${value}px`,
      }}
      className={styles.container}
      disableWrap
      disablePadding
    >
      <Card id={panelId} fullWidth>
        <CardContent>Main Panel</CardContent>
      </Card>
      <WindowSplitter
        {...splitterProps}
        aria-controls={panelId}
        aria-labelledby={panelId}
        disableFixed
      />
      <Card className={styles.second} fullWidth>
        <CardContent>Second Panel</CardContent>
      </Card>
    </Box>
  );
}

Press Enter to start editing.

@use "everything";

.container {
  grid-template-columns:
    minmax(0, everything.window-splitter-get-var(position))
    1fr;
  position: relative;
}

.second {
  flex-shrink: 0;
  white-space: nowrap;
}

Press Enter to start editing.

Relative Vertical Example

This is the same as above, but just vertical instead of horizontal.

Main Panel
Secondary Panel
"use client";

import { Box } from "@react-md/core/box/Box";
import { Card } from "@react-md/core/card/Card";
import { CardContent } from "@react-md/core/card/CardContent";
import { WindowSplitter } from "@react-md/core/window-splitter/WindowSplitter";
import { useWindowSplitter } from "@react-md/core/window-splitter/useWindowSplitter";
import { type ReactElement, useId } from "react";

export default function RelativeVerticalExample(): ReactElement {
  const panelId = useId();
  const { value, splitterProps } = useWindowSplitter({
    min: 52,
    max: 800,
    vertical: true,
    defaultValue: 52,
  });

  return (
    <Box
      stacked
      style={{
        "--rmd-window-splitter-position": `${value}px`,
        position: "relative",
      }}
      fullWidth
      disablePadding
    >
      <Card id={panelId} fullWidth style={{ height: value }}>
        <CardContent>Main Panel</CardContent>
      </Card>
      <WindowSplitter
        aria-controls={panelId}
        aria-labelledby={panelId}
        {...splitterProps}
        disableFixed
      />
      <Card fullWidth>
        <CardContent>Secondary Panel</CardContent>
      </Card>
    </Box>
  );
}

Press Enter to start editing.

Accessibility

The window splitter implements the window splitter specifications and will require an aria-label/aria-labelledby.

Keyboard Behavior