Skip to Content

Panels

Unstable: This component is marked unstable to help us gather feedback and improve it. Thus, there could be future breaking changes to this component.

Generic and simplistic component for most sliding multi-panel UIs.

import {
  unstable_Panels as Panels,
  List,
  ListItem,
  Surface,
  Text,
} from '@itwin/itwinui-react';

export default () => {
  const panelIdRoot = 'root';
  const panelIdMoreInfo = 'more-info';

  return (
    <Panels.Wrapper as={Surface} className='demo-panels-wrapper'>
      <Panels.Panel id={panelIdRoot}>
        <Surface.Header as={Panels.Header}>Root</Surface.Header>
        <Surface.Body as={List}>
          <ListItem>
            <Panels.Trigger for={panelIdMoreInfo}>
              <ListItem.Action>More details</ListItem.Action>
            </Panels.Trigger>
          </ListItem>
        </Surface.Body>
      </Panels.Panel>

      <Panels.Panel id={panelIdMoreInfo}>
        <Surface.Header as={Panels.Header}>More details</Surface.Header>
        <Surface.Body isPadded>
          <Text>Content</Text>
        </Surface.Body>
      </Panels.Panel>
    </Panels.Wrapper>
  );
};

Usage

Import

Since this API is released with the unstable flag, you can consider using an import alias to the make least amount of code changes when the API becomes stable.

import { unstable_Panels as Panels } from '@itwin/itwinui-react';

Composition API

You can build sliding multi-panel UIs with subcomponents that are fully customizable.

We provide four required subcomponents:

  • Panels.Wrapper: Wraps around all the Panels.Panel.
  • Panels.Panel: A single panel that can be shown or hidden depending on the active panel.
  • Panels.Header: The header of a panel that contains the title and the back button.
    • Provides the accessible name for the panel.
  • Panels.Trigger: Wrapper around an interactable element that transitions to a different panel when clicked.
<Panels.Wrapper>
<Panels.Panel id={panelIdRoot}>
<Panels.Header>Root</Panels.Header>
<Panels.Trigger for={panelIdMoreInfo}>
<Button>More details</Button>
</Panels.Trigger>
</Panels.Panel>
<Panels.Panel id={panelIdMoreInfo}>
<Panels.Header>More details</Panels.Header>
{/* Content */}
</Panels.Panel>
</Panels.Wrapper>

Polymorphic as prop

Panels is intentionally generic and does not provide much UI. That way you can use the as prop to render the panels to your needs and preferences. E.g. using Surface to render the panels:

<Panels.Wrapper
as={Surface}
style={{
inlineSize: 'min(300px, 30vw)',
blockSize: 'min(500px, 50vh)',
}}
>
<Panels.Panel id={panelIdRoot}>
<Surface.Header as={Panels.Header}>Root</Surface.Header>
<Surface.Body as={List}>
<ListItem>
<Panels.Trigger for={panelIdMoreInfo}>
<ListItem.Action>More details</ListItem.Action>
</Panels.Trigger>
</ListItem>
</Surface.Body>
</Panels.Panel>
<Panels.Panel id={panelIdMoreInfo}>
<Surface.Header as={Panels.Header}>More details</Surface.Header>
<Surface.Body isPadded>
<Text>Content</Text>
</Surface.Body>
</Panels.Panel>
</Panels.Wrapper>

Instance methods

You can use methods from Panels.useInstance() to control the state programmatically.

const panels = Panels.useInstance();
<Button onClick={() => panels.goBack()}>Go back</Button>
<Panels.Wrapper instance={panels}>{/* … */}</Panels.Wrapper>;
import {
  unstable_Panels as Panels,
  Flex,
  Surface,
  Button,
} from '@itwin/itwinui-react';

export default () => {
  const panels = Panels.useInstance();

  const initialActiveId = 'root';
  const panel1Id = 'panel-1';
  const panel1_1Id = 'panel-1-1';
  const panel1_1_1Id = 'panel-1-1-1';

  const panelIds = [initialActiveId, panel1Id, panel1_1Id, panel1_1_1Id];

  return (
    <Flex flexDirection='column' alignItems='flex-start'>
      <Button id='instance-go-back' onClick={() => panels.goBack()}>
        Go Back
      </Button>
      <Panels.Wrapper
        instance={panels}
        as={Surface}
        className='demo-panels-wrapper'
      >
        {panelIds.map((id, index) => (
          <Panels.Panel key={id} id={id}>
            <Surface.Header as={Panels.Header}>{id}</Surface.Header>
            <Surface.Body isPadded>
              <Panels.Trigger for={panelIds[index + 1]}>
                <Button>
                  Go to {panelIds[index + 1] ?? "panel that doesn't exist"}
                </Button>
              </Panels.Trigger>
            </Surface.Body>
          </Panels.Panel>
        ))}
      </Panels.Wrapper>
    </Flex>
  );
};

API Requirements and assumptions

The Panels API requires the following:

  • The initial displayed Panel should be the first Panel in the Panels.Wrapper.

    <Panels.Wrapper>
    {/* This is the first panel shown */}
    <Panels.Panel id='initial-panel'>{/* … */}</Panels.Panel>
    </Panels.Wrapper>
  • A panel can have only one trigger pointing to it. i.e. out of all the triggers across all panels, only one can point to a particular panel.

    <Panels.Wrapper>
    <Panels.Panel id='panel-1'>
    <Panels.Trigger for='panel-more-details'>More details</Panels.Trigger>
    </Panels.Panel>
    <Panels.Panel id='panel-2'>
    {/* This trigger should not exist since there is already is a trigger
    to panel-more-details in panel-1 */}
    <Panels.Trigger for='panel-more-details'>More details</Panels.Trigger>
    </Panels.Panel>
    <Panels.Panel id='panel-more-details'>{/* … */}</Panels.Panel>
    </Panels.Wrapper>
  • The Panels.Panels within the wrapper should be in the order of the navigation. E.g.:

    <Panels.Wrapper>
    {/* Must come before more-details since it contains the trigger to more-details */}
    <Panels.Panel id='root' />
    {/* Must come after root since it is navigated to from root */}
    <Panels.Panel id='more-details'>
    </Panels.Wrapper>

Examples

Multi-level List

import * as React from 'react';
import {
  unstable_Panels as Panels,
  List,
  ListItem,
  Surface,
  ToggleSwitch,
  Flex,
} from '@itwin/itwinui-react';

export default () => {
  const initialActiveId = React.useId();
  const qualityPanelId = React.useId();
  const speedPanelId = React.useId();
  const accessibilityPanelId = React.useId();

  const [repeat, setRepeat] = React.useState(false);
  const [quality, setQuality] = React.useState('240p');
  const [speed, setSpeed] = React.useState('1.0x');
  const [accessibilityOptions, setAccessibilityOptions] = React.useState([]);

  const panels = Panels.useInstance();

  const _Item = React.useCallback(
    ({ content, state, setState }) => {
      const selected = state === content;

      return (
        <ListItem
          active={selected}
          aria-selected={selected}
          onClick={() => {
            panels.goBack();
          }}
        >
          <ListItem.Action onClick={() => setState(content)}>
            {content}
          </ListItem.Action>
          <ListItem.Icon />
        </ListItem>
      );
    },
    [panels],
  );

  const _ItemQuality = React.useCallback(
    ({ content }) => (
      <_Item content={content} state={quality} setState={setQuality} />
    ),
    [_Item, quality],
  );

  const _ItemSpeed = React.useCallback(
    ({ content }) => (
      <_Item content={content} state={speed} setState={setSpeed} />
    ),
    [_Item, speed],
  );

  const _ItemAccessibility = React.useCallback(
    ({ content }) => (
      <_Item
        content={content}
        state={accessibilityOptions.includes(content) ? content : ''}
        setState={() => {
          setAccessibilityOptions((prev) =>
            prev.includes(content)
              ? prev.filter((item) => item !== content)
              : [...prev, content],
          );
        }}
      />
    ),
    [_Item, accessibilityOptions],
  );

  const qualities = React.useMemo(
    () => ['240p', '360p', '480p', '720p', '1080p'],
    [],
  );

  const speeds = React.useMemo(
    () => Array.from({ length: 21 }, (_, i) => (i * 0.1).toFixed(1) + 'x'),
    [],
  );

  const toggleSwitchId = React.useId();

  return (
    <>
      <Panels.Wrapper
        instance={panels}
        as={Surface}
        className='demo-panels-wrapper'
      >
        <Panels.Panel id={initialActiveId}>
          <List>
            <ListItem>
              <ListItem.Content as='label' htmlFor={toggleSwitchId}>
                Repeat
              </ListItem.Content>
              <ToggleSwitch
                id={toggleSwitchId}
                onChange={(e) => setRepeat(e.target.checked)}
                checked={repeat}
              />
            </ListItem>
            <ListItem>
              <Panels.Trigger for={qualityPanelId}>
                <ListItem.Action>Quality</ListItem.Action>
              </Panels.Trigger>
            </ListItem>
            <ListItem>
              <Panels.Trigger for={speedPanelId}>
                <ListItem.Action>Speed</ListItem.Action>
              </Panels.Trigger>
            </ListItem>
            <ListItem>
              <Panels.Trigger for={accessibilityPanelId}>
                <ListItem.Action>Accessibility</ListItem.Action>
              </Panels.Trigger>
            </ListItem>
          </List>
        </Panels.Panel>

        <Panels.Panel
          id={qualityPanelId}
          as={Flex}
          flexDirection='column'
          alignItems='stretch'
          gap='0'
        >
          <Surface.Header as={Panels.Header}>Quality</Surface.Header>
          <Surface.Body as={List}>
            {qualities.map((quality) => (
              <_ItemQuality key={quality} content={quality} />
            ))}
          </Surface.Body>
        </Panels.Panel>

        <Panels.Panel
          id={speedPanelId}
          as={Flex}
          flexDirection='column'
          alignItems='stretch'
          gap='0'
        >
          <Surface.Header as={Panels.Header}>Speed</Surface.Header>
          <Surface.Body as={List}>
            {speeds.map((speed) => (
              <_ItemSpeed key={speed} content={speed} />
            ))}
          </Surface.Body>
        </Panels.Panel>

        <Panels.Panel
          id={accessibilityPanelId}
          as={Flex}
          flexDirection='column'
          alignItems='stretch'
          gap='0'
        >
          <Surface.Header as={Panels.Header}>Accessibility</Surface.Header>
          <Surface.Body as={List}>
            <_ItemAccessibility content='High contrast' />
            <_ItemAccessibility content='Large text' />
            <_ItemAccessibility content='Screen reader' />
          </Surface.Body>
        </Panels.Panel>
      </Panels.Wrapper>
    </>
  );
};

Multi panel information panel

import {
  unstable_Panels as Panels,
  List,
  ListItem,
  Flex,
  Surface,
  Text,
  Divider,
} from '@itwin/itwinui-react';

export default () => {
  const initialActiveId = 'root';

  const panels = Array.from(Array(20).keys()).map((i) => ({
    id: `panel-${i}`,
    label: `Panel ${i}`,
  }));

  return (
    <Panels.Wrapper as={Surface} className='demo-panels-wrapper'>
      <Panels.Panel
        id={initialActiveId}
        as={Flex}
        flexDirection='column'
        alignItems='stretch'
        gap='0'
      >
        <Surface.Header as={Panels.Header}>Root</Surface.Header>
        <Surface.Body as={List}>
          {panels.map((panel) => (
            <ListItem key={panel.id}>
              <ListItem.Content>
                <Panels.Trigger for={`${panel.id}`}>
                  <ListItem.Action>{panel.label}</ListItem.Action>
                </Panels.Trigger>
              </ListItem.Content>
            </ListItem>
          ))}
        </Surface.Body>
      </Panels.Panel>

      {panels.map((panel) => (
        <Panels.Panel
          as={Flex}
          key={panel.id}
          id={panel.id}
          flexDirection='column'
          alignItems='stretch'
        >
          <Surface.Header as={Panels.Header}>{panel.label}</Surface.Header>
          <Surface.Body as={Flex} flexDirection='column'>
            <Text>{`Content for ${panel.id}`}</Text>
            <Flex.Spacer />
            <Divider />
            <Text>{`Footer for ${panel.id}`}</Text>
          </Surface.Body>
        </Panels.Panel>
      ))}
    </Panels.Wrapper>
  );
};

Props

Prop Description Default
onActiveIdChange
Function that gets called when the active panel is changed.
(newActiveId: string) => void
instance
Pass an instance created by useInstance to control the panels imperatively.
PanelsInstance
as
"symbol" | "object" | "div" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "button" | "canvas" | ... 158 more ... | FunctionComponent<...>