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.
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 thePanels.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.
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:
Instance methods
You can use methods from Panels.useInstance()
to control the state programmatically.
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 firstPanel
in thePanels.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.
-
The
Panels.Panel
s within the wrapper should be in the order of the navigation. E.g.:
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<...> |