Panel
Panel components provide a surface for contextual information alongside the main content area.Beta
New and ready to use. We'll provide comms and support for any major changes.
Internal use only
@atlassian/panel-system is an internal Atlassian package and is not available for external use.Basic
A panel is composed of smaller building blocks. At a minimum, wrap your content in a
PanelContainer and combine a PanelHeader (with a PanelTitle and optional PanelActionGroup),
a PanelContent/PanelBody for the main content, and an optional PanelFooter for primary
actions.
Basic panel example
This is a basic example of the panel system components.
The panel includes a header with a title and actions, and a body area for content.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Text } from '@atlaskit/primitives/compiled/text';
import { PanelActionClose } from '@atlassian/panel-system/panel-action/close';
import { PanelActionExpand } from '@atlassian/panel-system/panel-action/expand';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelActionMore } from '@atlassian/panel-system/panel-action/more';
import { PanelActionNewTab } from '@atlassian/panel-system/panel-action/new-tab';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { PanelFooter } from '@atlassian/panel-system/panel-footer';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
// This width is only set so the panel renders at a sensible size in the docs.
// Please use the navigation-system panel slot to size panels — do not hardcode this.
const styles = cssMap({
exampleWrapper: {
width: '400px',
height: '480px',
},
});
export default function BasicPanel(): React.JSX.Element {
return (
<Box xcss={styles.exampleWrapper}>
<PanelContainer testId="basic-panel">
<PanelHeader>
<PanelTitle>Basic panel example</PanelTitle>
<PanelActionGroup>
<PanelActionExpand onClick={() => {}} />
<PanelActionNewTab href="https://atlassian.design/components" />
<PanelActionMore onClick={() => {}} />
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelBody>
<Text as="p">This is a basic example of the panel system components.</Text>
<Text as="p">
The panel includes a header with a title and actions, and a body area for content.
</Text>
</PanelBody>
</PanelContent>
<PanelFooter>
<ButtonGroup label="Panel actions">
<Button appearance="subtle" onClick={() => {}}>
Cancel
</Button>
<Button appearance="primary" onClick={() => {}}>
Save
</Button>
</ButtonGroup>
</PanelFooter>
</PanelContainer>
</Box>
);
}Header
PanelHeader provides a consistent header area for a panel. Compose it with PanelTitle (with an
optional icon or app logo and back button) and a PanelActionGroup of panel actions such as expand,
open in new tab, more, and close.
Header title
Header title
App name
App name
Header title
import React from 'react';
import { cssMap, jsx } from '@atlaskit/css';
import InformationCircleIcon from '@atlaskit/icon/core/information-circle';
import { JiraIcon } from '@atlaskit/logo/jira-icon';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { token } from '@atlaskit/tokens';
import { PanelActionBack } from '@atlassian/panel-system/panel-action/back';
import { PanelActionClose } from '@atlassian/panel-system/panel-action/close';
import { PanelActionDown } from '@atlassian/panel-system/panel-action/down';
import { PanelActionExpand } from '@atlassian/panel-system/panel-action/expand';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelActionMore } from '@atlassian/panel-system/panel-action/more';
import { PanelActionNewTab } from '@atlassian/panel-system/panel-action/new-tab';
import { PanelActionUp } from '@atlassian/panel-system/panel-action/up';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
// Each header variation is wrapped in a bordered box purely for presentation in the docs.
// In a real panel, render `PanelHeader` directly inside a `PanelContainer`.
const styles = cssMap({
gallery: {
width: '400px',
},
headerWrapper: {
borderColor: token('color.border'),
borderStyle: 'solid',
borderWidth: token('border.width'),
borderRadius: token('radius.small'),
overflow: 'hidden',
},
});
function HeaderVariant({ children }: { children: React.ReactNode }) {
return <Box xcss={styles.headerWrapper}>{children}</Box>;
}
export default function Headers(): React.JSX.Element {
return (
<Stack space="space.300" xcss={styles.gallery}>
{/* Basic */}
<HeaderVariant>
<PanelHeader>
<PanelTitle>Header title</PanelTitle>
<PanelActionGroup>
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Basic + title icon */}
<HeaderVariant>
<PanelHeader>
<PanelTitle icon={<InformationCircleIcon label="" />}>Header title</PanelTitle>
<PanelActionGroup>
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Branded */}
<HeaderVariant>
<PanelHeader>
<PanelTitle icon={<JiraIcon size="xsmall" appearance="brand" />}>App name</PanelTitle>
<PanelActionGroup>
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Branded + back button */}
<HeaderVariant>
<PanelHeader>
<PanelActionBack onClick={() => {}} />
<PanelTitle icon={<JiraIcon size="xsmall" appearance="brand" />}>App name</PanelTitle>
<PanelActionGroup>
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Branded + panel action */}
<HeaderVariant>
<PanelHeader>
<PanelTitle icon={<JiraIcon size="xsmall" appearance="brand" />}>App name</PanelTitle>
<PanelActionGroup>
<PanelActionNewTab href="#" />
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Branded + app panel + 2 panel actions */}
<HeaderVariant>
<PanelHeader>
<PanelTitle icon={<JiraIcon size="xsmall" appearance="brand" />}>App name</PanelTitle>
<PanelActionGroup>
<PanelActionMore onClick={() => {}} />
<PanelActionExpand onClick={() => {}} />
<PanelActionNewTab href="#" />
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
{/* Navigation actions come before all other header actions. */}
<HeaderVariant>
<PanelHeader>
<PanelTitle>Header title</PanelTitle>
<PanelActionGroup>
<PanelActionUp onClick={() => {}} />
<PanelActionDown onClick={() => {}} />
<PanelActionMore onClick={() => {}} />
<PanelActionClose onClick={() => {}} />
</PanelActionGroup>
</PanelHeader>
</HeaderVariant>
</Stack>
);
}This Jira-style example keeps the list selection and panel content in sync. Open a work item, then use the up/down actions to move to the previous or next item in list order. Navigation wraps at both ends. Closing the panel returns focus to the current work item in the list.
The host owns the selected item; both actions come first in PanelActionGroup. The view switcher
keeps the current item selected when moving between Side and Center views. Scroll the details to see
the compact sticky subheader with the current title and status.
PanelFooter anchors actions to the bottom of a panel. Actions can be right aligned or spread out,
and an optional PanelDisclaimer can be added below the actions in one or two zones.
Layout
Panel system is designed to be rendered inside the LayoutWithPanel component. This should live
inside navigation system's Main slot. Panels opened via
Trigger, or programmatically will automatically render inside LayoutWithPanel docked to the side
of the page with consistent layout, spacing, and resize behaviour. This example shows a complete
side panel — including a PanelSubheader, a cover image, and a PanelDisclaimer — rendered through
LayoutWithPanel.
import { useEffect } from 'react';
import Button from '@atlaskit/button/default/button';
import IconButton from '@atlaskit/button/icon/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import { cssMap, jsx } from '@atlaskit/css';
import Heading from '@atlaskit/heading/heading';
import PageIcon from '@atlaskit/icon/core/page';
import ThumbsDownIcon from '@atlaskit/icon/core/thumbs-down';
import ThumbsUpIcon from '@atlaskit/icon/core/thumbs-up';
import { ConfluenceIcon } from '@atlaskit/logo/confluence-icon';
import { Main } from '@atlaskit/navigation-system/layout/main';
import { Root } from '@atlaskit/navigation-system/layout/root';
import { Anchor } from '@atlaskit/primitives/compiled/anchor';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { LayoutWithPanel } from '@atlassian/panel-system/layout-with-panel';
import { PanelActionBack } from '@atlassian/panel-system/panel-action/back';
import { PanelActionClose } from '@atlassian/panel-system/panel-action/close';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelActionMore } from '@atlassian/panel-system/panel-action/more';
import { PanelActionNewTab } from '@atlassian/panel-system/panel-action/new-tab';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { PanelDisclaimer } from '@atlassian/panel-system/panel-disclaimer';
import { PanelFooter } from '@atlassian/panel-system/panel-footer';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { usePanelActions } from '@atlassian/panel-system/panel-manager';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelSubheader } from '@atlassian/panel-system/panel-subheader';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
import { PanelCoverImage } from '../images/panel-cover';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
mainContent: {
flex: 1,
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
},
});
export function SidePanelExample(): JSX.Element {
return (
<PanelProvider>
<RelayEnvironmentWrapper>
<Root>
<Main>
<AppContent />
</Main>
</Root>
</RelayEnvironmentWrapper>
</PanelProvider>
);
}
const AppContent = () => {
const manager = usePanelActions();
useEffect(() => {
// Prefer <Trigger/> with entry points for loading and focus management.
manager.openPanel({
instanceId: 'confluence-panel',
panelContent: <Panel />,
});
}, [manager]);
return (
<LayoutWithPanel>
<Box xcss={styles.mainContent}>
<Stack space="space.200">
<Heading size="medium">Main content area</Heading>
<Text>
Panel system is designed to be rendered inside the navigation system's `Panel` slot.
Compose your panel with `PanelContainer` and place it inside `Panel` so it sits
alongside the page's `Main` content with consistent layout and resize behaviour.
</Text>
</Stack>
</Box>
</LayoutWithPanel>
);
};
const Panel = () => {
return (
<PanelContainer testId="preview-panel">
<PanelHeader>
<PanelActionBack onClick={() => {}} testId="back-action" />
<PanelTitle icon={<ConfluenceIcon size="xsmall" />}>Confluence</PanelTitle>
<PanelActionGroup>
<PanelActionNewTab
href="/projects/blueshift"
onClick={() => {}}
testId="new-tab-action"
/>
<PanelActionMore onClick={() => {}} testId="more-action" />
<PanelActionClose onClick={() => {}} testId="close-action" />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelSubheader
title="Subheading title"
titleIcon={<PageIcon label="" />}
coverImage={<PanelCoverImage />}
/>
<PanelBody>
<Stack space="space.200">
<Heading size="small" as="h3">
Project overview
</Heading>
<Text>
Panel system is a set of platform components to standardize the look and feel of
panels in Atlassian products.
</Text>
<Heading size="xsmall" as="h4">
Key features
</Heading>
<Box as="ul">
<Box as="li">Unified design system components</Box>
<Box as="li">Consistent navigation patterns</Box>
<Box as="li">Improved accessibility standards</Box>
</Box>
<Heading size="xsmall" as="h4">
Long content (this should wrap)
</Heading>
<Anchor href="https://example.atlassian.net/browse/longnamelongnamelongnamelongnamelongnamelongnamelongnamelongnamelongname">
https://example.atlassian.net/browse/longnamelongnamelongnamelongnamelongnamelongnamelongnamelongnamelongname
</Anchor>
</Stack>
</PanelBody>
</PanelContent>
<PanelFooter
disclaimer={
<PanelDisclaimer>
<Flex justifyContent="space-between" alignItems="center">
<Flex gap="space.025">
<IconButton
icon={ThumbsUpIcon}
label="Helpful"
appearance="subtle"
spacing="compact"
onClick={() => {}}
/>
<IconButton
icon={ThumbsDownIcon}
label="Not helpful"
appearance="subtle"
spacing="compact"
onClick={() => {}}
/>
</Flex>
<Text size="small" color="color.text.subtle">
Powered by Rovo AI. Verify results.
</Text>
</Flex>
</PanelDisclaimer>
}
>
<Button appearance="primary" onClick={() => {}}>
Save
</Button>
<Checkbox label="Create another" isChecked={false} onChange={() => {}} />
</PanelFooter>
</PanelContainer>
);
};
export default SidePanelExample;Subheader
Use PanelSubheader to display a secondary heading beneath the header. It supports a coverImage
for displaying a banner image at the top of the panel, and inline editing, allowing people to update
fields such as a title directly from within the panel. Use stickyContent to provide any React
content that appears after the regular subheader scrolls out of view.
Page details
Project overview
The subheader title above is editable in place. Select it to enter edit mode, change the text, then confirm or cancel.import React, { useState } from 'react';
import Breadcrumbs from '@atlaskit/breadcrumbs/breadcrumbs';
import { BreadcrumbsCurrentItem } from '@atlaskit/breadcrumbs/breadcrumbs-current-item';
import { BreadcrumbsItem } from '@atlaskit/breadcrumbs/breadcrumbs-item';
import { cssMap } from '@atlaskit/css';
import Heading from '@atlaskit/heading/heading';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { PanelActionClose } from '@atlassian/panel-system/panel-action/close';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelSubheader } from '@atlassian/panel-system/panel-subheader';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
import { SubheaderInlineEdit } from '@atlassian/panel-system/subheader-inline-edit';
import { PanelCoverImage } from '../images/panel-cover';
const subheaderBreadcrumbs = (
<Breadcrumbs size="small">
<BreadcrumbsItem href="#" text="Workspace" />
<BreadcrumbsItem href="#" text="Projects" />
<BreadcrumbsItem href="#" text="Team space" />
<BreadcrumbsCurrentItem href="#" text="CAT-2591" />
</Breadcrumbs>
);
// This width is only set so the panel renders at a sensible size in the docs.
// Please use the navigation-system panel slot to size panels — do not hardcode this.
const styles = cssMap({
exampleWrapper: {
width: '400px',
height: '480px',
},
});
export function SubheaderInlineEditExample(): React.JSX.Element {
const [title, setTitle] = useState('Subheading title');
return (
<Box xcss={styles.exampleWrapper}>
<PanelContainer testId="preview-panel">
<PanelHeader>
<PanelTitle>Page details</PanelTitle>
<PanelActionGroup>
<PanelActionClose onClick={() => {}} testId="close-action" />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelSubheader
title={title}
breadcrumbs={subheaderBreadcrumbs}
coverImage={<PanelCoverImage />}
inlineEdit={
<SubheaderInlineEdit
defaultValue={title}
placeholder="Give this panel a title"
editButtonLabel="Edit subheading title"
onConfirm={(value) => setTitle(value)}
testId="subheader-inline-edit"
/>
}
/>
<PanelBody>
<Stack space="space.200">
<Heading size="small" as="h3">
Project overview
</Heading>
<Text>
The subheader title above is editable in place. Select it to enter edit mode, change
the text, then confirm or cancel.
</Text>
</Stack>
</PanelBody>
</PanelContent>
</PanelContainer>
</Box>
);
}
export default SubheaderInlineEditExample;Resizing
When a panel is rendered within the navigation system layout, it
can be resized using the PanelSplitter. This lets people give more or less screen space to the
panel as needed.
import React from 'react';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import { cssMap } from '@atlaskit/css';
import { Main } from '@atlaskit/navigation-system/layout/main';
import { Panel } from '@atlaskit/navigation-system/layout/panel';
import { PanelSplitter } from '@atlaskit/navigation-system/layout/panel-splitter';
import { Root } from '@atlaskit/navigation-system/layout/root';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { PanelFooter } from '@atlassian/panel-system/panel-footer';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelSubheader } from '@atlassian/panel-system/panel-subheader';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
import { PanelCoverImage } from '../images/panel-cover';
const styles = cssMap({
mainContent: {
flex: 1,
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
},
});
export function ResizingExample(): JSX.Element {
return (
<Root>
<Main>
<Box xcss={styles.mainContent}>
<Text>
Click and drag the left edge of the panel to resize it. The PanelSplitter from
@atlaskit/navigation-system handles all resize logic.
</Text>
</Box>
</Main>
<Panel defaultWidth={400} label="Resizable panel">
<PanelContainer>
<PanelHeader>
<PanelTitle>Resizable Panel</PanelTitle>
</PanelHeader>
<PanelContent>
<PanelSubheader title="Subheading title" coverImage={<PanelCoverImage />} />
<PanelBody>
<Text>This is a simple example showing the resize functionality.</Text>
</PanelBody>
</PanelContent>
<PanelFooter>
<Checkbox label="Create another" isChecked={false} onChange={() => {}} />
<Button appearance="primary" onClick={() => {}}>
Create
</Button>
</PanelFooter>
</PanelContainer>
<PanelSplitter label="Resize panel" />
</Panel>
</Root>
);
}
export default ResizingExample;Default loading state
When using PanelRouter, a loading skeleton is provided by default which offers a basic outline of
a panel with a header, subheader, and body content.
import { cssMap, jsx } from '@atlaskit/css';
import { Box } from '@atlaskit/primitives/compiled/box';
import { PanelSkeleton } from '@atlassian/panel-system/panel-skeleton';
// This width is only set so the panel renders at a sensible size in the docs.
// Please use the navigation-system panel slot to size panels — do not hardcode this.
// In a real panel, the default skeleton is provided automatically by `PanelRouter`.
const styles = cssMap({
exampleWrapper: {
width: '400px',
height: '480px',
},
});
export function LoadingDefault(): JSX.Element {
return (
<Box xcss={styles.exampleWrapper}>
<PanelSkeleton />
</Box>
);
}
export default LoadingDefault;Custom loading state
You can also create your own loading skeleton by composing some ready-made components:
PanelHeaderSkeleton, PanelSubheaderSkeleton, PanelBodySkeleton, and PanelFooterSkeleton.
These can also be used with your own mix of skeleton components. Each of these components implements
the same spacing as their respective layout components, so PanelHeaderSkeleton mimics
PanelHeader and so on. Provide your custom loading state to PanelRouter's fallback prop.
import { cssMap, jsx } from '@atlaskit/css';
import { Box } from '@atlaskit/primitives/compiled/box';
import { PanelBodySkeleton } from '@atlassian/panel-system/panel-body-skeleton';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { PanelFooterSkeleton } from '@atlassian/panel-system/panel-footer-skeleton';
import { PanelHeaderSkeleton } from '@atlassian/panel-system/panel-header-skeleton';
import { PanelSubheaderSkeleton } from '@atlassian/panel-system/panel-subheader-skeleton';
// This width is only set so the panel renders at a sensible size in the docs.
// Please use the navigation-system panel slot to size panels — do not hardcode this.
// In a real panel, pass your custom skeleton to `PanelRouter`'s `fallback` prop.
const styles = cssMap({
exampleWrapper: {
width: '400px',
height: '480px',
},
});
export function LoadingComposable(): JSX.Element {
return (
<Box xcss={styles.exampleWrapper}>
<PanelContainer testId="panel-container-skeleton">
<PanelHeaderSkeleton shouldShowIcon actionsCount={3} />
<PanelContent>
<PanelSubheaderSkeleton shouldShowIcon />
<PanelBodySkeleton />
</PanelContent>
<PanelFooterSkeleton actionsCount={2} />
</PanelContainer>
</Box>
);
}
export default LoadingComposable;Panel manager
The PanelProvider, PanelRouter, and usePanelManager APIs let you manage panel state
declaratively. Use the manager to open, replace, and stack panels, and to navigate between different
panel EntryPoints.
import React, { useCallback, useEffect, useRef } from 'react';
import { type EntryPoint, loadEntryPoint } from 'react-relay';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { useId } from '@atlaskit/ds-lib/use-id';
import Heading from '@atlaskit/heading/heading';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { usePanelManager } from '@atlassian/panel-system';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelRouter } from '@atlassian/panel-system/panel-router';
import { PanelsRenderer } from '@atlassian/panel-system/panels-renderer';
import { useRelayEnvironmentProvider } from '@atlassian/relay-environment-provider';
import { panelEntryPoint } from '../utils/dummy-entrypoint';
import DummyEntryPointComponent from '../utils/dummy-entrypoint-component';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
layout: {
height: '100vh',
width: '100%',
},
mainContent: {
flex: 1,
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
},
panelWrapper: {
borderInlineStart: `${token('border.width')} solid ${token('color.border')}`,
width: '480px',
},
buttonGroup: {
display: 'flex',
gap: token('space.100'),
flexWrap: 'wrap',
},
stateDisplay: {
paddingBlockStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockEnd: token('space.200'),
paddingInlineStart: token('space.200'),
backgroundColor: token('color.background.neutral'),
},
});
// Helper to get entry point name for display
function getEntryPointName(entryPoint: EntryPoint<any, any>): string {
const root = entryPoint.root as any;
return root?.getModuleName?.() || root?.getModuleId?.() || 'unknown-entrypoint';
}
function PanelManagerExample() {
const { state, manager } = usePanelManager();
const baseId = useId();
const panelCounterRef = useRef(0);
const environmentProvider = useRelayEnvironmentProvider();
const openPageLoadPanel = useCallback(
(reference: any) => {
// Opening a panel replaces the current content.
panelCounterRef.current += 1;
const instanceId = `${baseId}-pageload-${panelCounterRef.current}`;
manager.openPanel({
instanceId,
entryPoint: panelEntryPoint,
params: { conversation: 'main' },
entryPointReference: reference,
});
},
[manager, baseId],
);
const openNewPanel = useCallback(() => {
panelCounterRef.current += 1;
const instanceId = `${baseId}-new-${panelCounterRef.current}`;
manager.openPanel({
instanceId,
panelContent: <DummyEntryPointComponent />,
params: { pageId: 'page-789' },
});
}, [manager, baseId]);
// Open a panel on mount for demonstration
useEffect(() => {
// Mimic the behaviour of loading an entrypoint on page load
const entryPointReference = loadEntryPoint<typeof panelEntryPoint>(
environmentProvider,
panelEntryPoint,
{},
);
if (entryPointReference && state.activePanel === null) {
openPageLoadPanel(entryPointReference);
}
}, [environmentProvider]);
return (
<Flex xcss={styles.layout}>
<Box xcss={styles.mainContent}>
<Stack space="space.300">
<Heading size="medium">Panel Manager + PanelsRenderer Example</Heading>
<Text>
This example demonstrates using both the panel manager for programmatic control and
PanelsRenderer for automatic rendering. Use the manager to open/close panels and read
state, while PanelsRenderer handles rendering automatically. The `openPanel` method is
used to ensure only one panel is open at a time (single panel mode).
</Text>
<Text>An EntryPoint panel opens by default on page load.</Text>
<Stack space="space.200">
<Heading size="small" as="h3">
Actions
</Heading>
<Box xcss={styles.buttonGroup}>
<Button onClick={openNewPanel} testId="open-new-button">
Open New Panel
</Button>
</Box>
</Stack>
<Stack space="space.200">
<Heading size="small" as="h3">
Active panel
</Heading>
{state.activePanel === null ? (
<Text color="color.text.subtle">No active panel</Text>
) : (
<Box xcss={styles.stateDisplay}>
<pre>
{JSON.stringify(
{
instanceId: state.activePanel.instanceId,
entryPoint:
'entryPoint' in state.activePanel && state.activePanel.entryPoint
? getEntryPointName(state.activePanel.entryPoint)
: 'N/A',
params: state.activePanel.params,
},
null,
2,
)}
</pre>
</Box>
)}
</Stack>
</Stack>
</Box>
<PanelsRenderer>
<PanelRouter
wrapper={({ children }: { children: React.ReactNode }) => (
<Box xcss={styles.panelWrapper}>{children}</Box>
)}
errorFallback={() => <Text as="p">Error loading panel</Text>}
/>
</PanelsRenderer>
</Flex>
);
}
export default function PanelManagerExampleWithProvider(): React.JSX.Element {
return (
<PanelProvider>
<RelayEnvironmentWrapper>
<PanelManagerExample />
</RelayEnvironmentWrapper>
</PanelProvider>
);
}Custom panel rendering
You can customize how panels are rendered and prefetch panel EntryPoints ahead of time for a faster
experience. This example uses useCurrentPanel together with the panel manager to render different
panels from a shared renderer.
import React, { type Ref, useEffect, useRef } from 'react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { useId } from '@atlaskit/ds-lib/use-id';
import Heading from '@atlaskit/heading/heading';
import { Main } from '@atlaskit/navigation-system/layout/main';
import { Panel } from '@atlaskit/navigation-system/layout/panel';
import { PanelSplitter } from '@atlaskit/navigation-system/layout/panel-splitter';
import { Root } from '@atlaskit/navigation-system/layout/root';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { panelEntryPoint } from '@atlassian/entry-point-fixtures/panelEntryPoint';
import { usePanelManager } from '@atlassian/panel-system';
import { PanelActionCloseSmart } from '@atlassian/panel-system/panel-action/close-smart';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { useCurrentPanel } from '@atlassian/panel-system/panel-context';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
import { PanelsRenderer } from '@atlassian/panel-system/panels-renderer';
import { Trigger } from '@atlassian/panel-system/trigger';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
mainContent: {
flex: 1,
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
},
buttonGroup: {
display: 'flex',
gap: token('space.100'),
flexWrap: 'wrap',
},
});
// Use real entry point from fixtures for prefetching demonstration
const userPanelEntryPoint = panelEntryPoint;
const settingsPanelEntryPoint = panelEntryPoint;
const notificationsPanelEntryPoint = panelEntryPoint;
type UserPanelParams = {
userId: string;
userName: string;
};
function CustomUserPanel() {
const panel = useCurrentPanel<UserPanelParams>();
const userId = panel.params.userId;
const userName = panel.params.userName;
return (
<PanelContainer>
<PanelHeader>
<PanelTitle>User Profile</PanelTitle>
<PanelActionGroup>
<PanelActionCloseSmart />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelBody>
<Stack space="space.200">
<Heading size="small" as="h3">
{userName}
</Heading>
<Text color="color.text.subtle">ID: {userId}</Text>
<Text size="small">
This panel was opened via context-based customisation. The component accesses panel
data cleanly using the useCurrentPanel() hook. Drag the left edge to resize.
</Text>
</Stack>
</PanelBody>
</PanelContent>
</PanelContainer>
);
}
type NotificationsPanelParams = {
count: number;
};
function NotificationsPanel() {
const panel = useCurrentPanel<NotificationsPanelParams>();
const count = panel.params.count;
return (
<PanelContainer>
<PanelHeader>
<PanelTitle>Notifications</PanelTitle>
<PanelActionGroup>
<PanelActionCloseSmart />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelBody>
<Stack space="space.200">
<Heading size="small" as="h3">
You have {count} notifications
</Heading>
<Text size="small">Drag the left edge to resize this panel.</Text>
</Stack>
</PanelBody>
</PanelContent>
</PanelContainer>
);
}
type SettingsPanelParams = {
category: string;
};
function CustomSettingsPanel() {
const panel = useCurrentPanel<SettingsPanelParams>();
const category = panel.params.category;
return (
<PanelContainer>
<PanelHeader>
<PanelTitle>Settings</PanelTitle>
<PanelActionGroup>
<PanelActionCloseSmart />
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelBody>
<Stack space="space.200">
<Heading size="small" as="h3">
{category || 'General'} Settings
</Heading>
<Text size="small">
This is a custom panel rendering. You can build any UI you want using the panel UI
components while accessing panel data via context. Drag the left edge to resize.
</Text>
</Stack>
</PanelBody>
</PanelContent>
</PanelContainer>
);
}
function PanelRouter() {
const panel = useCurrentPanel();
const { instanceId } = panel;
if (instanceId.includes('user-panel')) {
return <CustomUserPanel />;
}
if (instanceId.includes('settings-panel')) {
return <CustomSettingsPanel />;
}
if (instanceId.includes('notifications-panel')) {
return <NotificationsPanel />;
}
return null;
}
function CustomPanelRenderingExample() {
const baseId = useId();
const { state, manager } = usePanelManager();
const hasOpenedRef = useRef(false);
const isPanelOpen = state.activePanel !== null;
// Open a panel on mount for demonstration
useEffect(() => {
if (!hasOpenedRef.current && state.activePanel === null) {
hasOpenedRef.current = true;
manager.openPanel({
instanceId: `${baseId}-user-panel`,
entryPoint: userPanelEntryPoint,
params: {
userId: 'user-123',
userName: 'Alice Johnson',
},
});
}
}, []);
return (
<Root>
<Main>
<Box xcss={styles.mainContent}>
<Stack space="space.300">
<Heading size="medium">Custom Panel Rendering with Context and Resizing</Heading>
<Text>
This example demonstrates the context-based customisation pattern with resizing. Each
trigger opens a panel, and custom panel components use useCurrentPanel() to access
their data. Drag the left edge of any panel to resize it.
</Text>
<Stack space="space.200">
<Heading size="small" as="h3">
Open Panels
</Heading>
<Text color="color.text.subtle" size="small">
Click to open different panel types. Each uses context-based customisation.
</Text>
<Box xcss={styles.buttonGroup}>
<Trigger
panel={userPanelEntryPoint}
entryPointProps={{ userId: 'user-123', userName: 'Alice Johnson' }}
instanceId={`${baseId}-user-panel`}
>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-user">
Open User Profile
</Button>
)}
</Trigger>
<Trigger
panel={settingsPanelEntryPoint}
entryPointProps={{ category: 'Preferences' }}
instanceId={`${baseId}-settings-panel`}
>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-settings">
Open Settings
</Button>
)}
</Trigger>
<Trigger
panel={notificationsPanelEntryPoint}
entryPointProps={{ count: 5 }}
instanceId={`${baseId}-notifications-panel`}
>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-notifications">
Open Notifications
</Button>
)}
</Trigger>
</Box>
</Stack>
</Stack>
</Box>
</Main>
{isPanelOpen && (
<Panel defaultWidth={400} label="Panel">
<PanelsRenderer>
<PanelRouter />
</PanelsRenderer>
<PanelSplitter label="Resize panel" />
</Panel>
)}
</Root>
);
}
export default function CustomPanelRenderingExampleWithProvider(): React.JSX.Element {
return (
<RelayEnvironmentWrapper>
<PanelProvider>
<CustomPanelRenderingExample />
</PanelProvider>
</RelayEnvironmentWrapper>
);
}EntryPoint loading
Panels should load their content lazily through an EntryPoint, so the code for a panel is only
fetched when it is opened. Use a Trigger with a panel EntryPoint to open a panel and render it
through PanelsRenderer and PanelRouter.
import React, { type Ref } from 'react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import Heading from '@atlaskit/heading/heading';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { panelEntryPoint } from '@atlassian/entry-point-fixtures/panelEntryPoint';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelRouter } from '@atlassian/panel-system/panel-router';
import { PanelsRenderer } from '@atlassian/panel-system/panels-renderer';
import { Trigger } from '@atlassian/panel-system/trigger';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
layout: {
height: '100vh',
width: '100%',
overflow: 'hidden',
},
mainContent: {
flex: 1,
minWidth: '0px',
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
overflow: 'auto',
},
buttonGroup: {
display: 'flex',
gap: token('space.100'),
flexWrap: 'wrap',
},
panelWrapper: {
borderInlineStart: `${token('border.width')} solid ${token('color.border')}`,
flexShrink: 0,
overflow: 'hidden',
height: '100vh',
display: 'flex',
flexDirection: 'column',
width: '400px',
},
});
function EntryPointLoadingExample() {
return (
<PanelProvider>
<Flex xcss={styles.layout}>
<Box xcss={styles.mainContent}>
<Stack space="space.300">
<Heading size="medium">Entry Point Loading Example</Heading>
<Text>This example demonstrates entry point loading using a simple entry point.</Text>
<Stack space="space.200">
<Heading size="small" as="h3">
Open Panel
</Heading>
<Text color="color.text.subtle" size="small">
Click to open a panel. The panel loads the entry point.
</Text>
<Box xcss={styles.buttonGroup}>
<Trigger panel={panelEntryPoint} params={{}}>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-panel">
Open Panel
</Button>
)}
</Trigger>
</Box>
</Stack>
</Stack>
</Box>
<PanelsRenderer>
<PanelRouter
wrapper={({ children }: { children: React.ReactNode }) => (
<Box xcss={styles.panelWrapper}>{children}</Box>
)}
errorFallback={() => <Text as="p">Error loading panel</Text>}
/>
</PanelsRenderer>
</Flex>
</PanelProvider>
);
}
export default function EntryPointLoadingExampleWithProvider(): React.JSX.Element {
return (
<RelayEnvironmentWrapper>
<EntryPointLoadingExample />
</RelayEnvironmentWrapper>
);
}Loading methods
There are several ways to open a panel: declaratively with a Trigger, or imperatively through the
panel manager (for example manager.openPanel). This example demonstrates the different loading
methods side by side.
Trigger is the recommended loading method as it handles preloading on hover/focus automatically.
import React, { type Ref } from 'react';
import Button from '@atlaskit/button/default/button';
import Code from '@atlaskit/code/code';
import { cssMap } from '@atlaskit/css';
import Heading from '@atlaskit/heading/heading';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Inline } from '@atlaskit/primitives/compiled/inline';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { usePanelManager } from '@atlassian/panel-system';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelRouter } from '@atlassian/panel-system/panel-router';
import { PanelsRenderer } from '@atlassian/panel-system/panels-renderer';
import { Trigger } from '@atlassian/panel-system/trigger';
import { panelEntryPoint } from '../utils/dummy-entrypoint';
import DummyEntryPointComponent from '../utils/dummy-entrypoint-component';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
layout: {
height: '100vh',
width: '100%',
overflow: 'hidden',
},
mainContent: {
flex: 1,
minWidth: '0px',
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
overflow: 'auto',
},
panelWrapper: {
borderInlineStart: `${token('border.width')} solid ${token('color.border')}`,
flexShrink: 0,
overflow: 'hidden',
height: '100vh',
display: 'flex',
flexDirection: 'column',
width: '400px',
},
});
function LoadingMethodsExample() {
const { manager } = usePanelManager();
const onClickWithoutEntrypoint = () => {
manager.openPanel({
instanceId: 'panelWithoutEntryPoint',
panelContent: <DummyEntryPointComponent />,
});
};
return (
<Flex xcss={styles.layout}>
<Box xcss={styles.mainContent}>
<Stack space="space.300">
<Heading size="medium">All loading methods</Heading>
<Text>This example demonstrates all ways to load a panel</Text>
<Stack space="space.200">
<Heading size="small" as="h3">
Open Panel
</Heading>
<Text color="color.text.subtle" size="small">
Click to open a panel. The panel loads the entry point.
</Text>
<Stack space="space.200">
<Inline space="space.100" alignBlock="baseline">
<Text>Open panel via Trigger</Text>
<Trigger panel={panelEntryPoint} params={{}}>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-panel-1">
Open panel
</Button>
)}
</Trigger>
</Inline>
<Inline space="space.100" alignBlock="baseline">
<Text>
Open panel via <Code>manager.openPanel()</Code>
</Text>
<Button onClick={onClickWithoutEntrypoint} testId="open-panel-3">
Open panel
</Button>
</Inline>
</Stack>
</Stack>
</Stack>
</Box>
<PanelsRenderer>
<PanelRouter
wrapper={({ children }: { children: React.ReactNode }) => (
<Box xcss={styles.panelWrapper}>{children}</Box>
)}
errorFallback={() => <Text as="p">Error loading panel</Text>}
/>
</PanelsRenderer>
</Flex>
);
}
export default function LoadingMethodsExampleWithProvider(): React.JSX.Element {
return (
<PanelProvider>
<RelayEnvironmentWrapper>
<LoadingMethodsExample />
</RelayEnvironmentWrapper>
</PanelProvider>
);
}Unsaved changes
Use the onBeforeClose callback to intercept a close request. Return false to keep the panel open
— for example, to show a confirmation dialog when there are unsaved changes.
import React, { type Ref, useCallback, useState } from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import Heading from '@atlaskit/heading/heading';
import ModalBody from '@atlaskit/modal-dialog/modal-body';
import Modal from '@atlaskit/modal-dialog/modal-dialog';
import ModalFooter from '@atlaskit/modal-dialog/modal-footer';
import ModalHeader from '@atlaskit/modal-dialog/modal-header';
import ModalTitle from '@atlaskit/modal-dialog/modal-title';
import ModalTransition from '@atlaskit/modal-dialog/modal-transition';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import { token } from '@atlaskit/tokens';
import { panelEntryPoint } from '@atlassian/entry-point-fixtures/panelEntryPoint';
import { PanelActionClose } from '@atlassian/panel-system/panel-action/close';
import { PanelActionGroup } from '@atlassian/panel-system/panel-action/group';
import { PanelBody } from '@atlassian/panel-system/panel-body';
import { PanelContainer } from '@atlassian/panel-system/panel-container';
import { PanelContent } from '@atlassian/panel-system/panel-content';
import { useCloseCurrentPanel } from '@atlassian/panel-system/panel-context';
import { PanelFooter } from '@atlassian/panel-system/panel-footer';
import { PanelHeader } from '@atlassian/panel-system/panel-header';
import { PanelProvider } from '@atlassian/panel-system/panel-provider';
import { PanelTitle } from '@atlassian/panel-system/panel-title';
import { PanelsRenderer } from '@atlassian/panel-system/panels-renderer';
import { Trigger } from '@atlassian/panel-system/trigger';
import { RelayEnvironmentWrapper } from '../utils/relay-environment-wrapper';
const styles = cssMap({
layout: {
height: '100vh',
width: '100%',
},
mainContent: {
flex: 1,
paddingBlockStart: token('space.300'),
paddingInlineEnd: token('space.300'),
paddingBlockEnd: token('space.300'),
paddingInlineStart: token('space.300'),
backgroundColor: token('color.background.neutral.subtle'),
},
buttonGroup: {
display: 'flex',
gap: token('space.100'),
flexWrap: 'wrap',
},
panelWrapper: {
borderInlineStart: `${token('border.width')} solid ${token('color.border')}`,
width: '480px',
},
});
/**
* Panel component that demonstrates unsaved changes handling with onBeforeClose.
* This shows how to prevent panel closing when there are unsaved changes, and how
* to show a confirmation dialog using @atlaskit/modal-dialog.
*/
function UnsavedChangesPanel() {
const closePanel = useCloseCurrentPanel();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [formValue, setFormValue] = useState('');
const [showDiscardModal, setShowDiscardModal] = useState(false);
const handleBeforeClose = async () => {
if (hasUnsavedChanges) {
// Show the discard confirmation modal instead of using window.confirm
setShowDiscardModal(true);
return false; // Prevent panel from closing
}
return true;
};
const handleDiscard = useCallback(() => {
setShowDiscardModal(false);
setHasUnsavedChanges(false);
setFormValue('');
closePanel();
}, [closePanel]);
const handleCancelDiscard = useCallback(() => {
setShowDiscardModal(false);
}, []);
const handleSave = () => {
setHasUnsavedChanges(false);
};
const handleCancel = () => {
setFormValue('');
setHasUnsavedChanges(false);
};
return (
<>
<Box xcss={styles.panelWrapper}>
<PanelContainer testId="unsaved-changes-panel">
<PanelHeader>
<PanelTitle>Edit Form</PanelTitle>
<PanelActionGroup>
<PanelActionClose
onBeforeClose={handleBeforeClose}
onClick={closePanel}
testId="close-button"
/>
</PanelActionGroup>
</PanelHeader>
<PanelContent>
<PanelBody>
<Stack space="space.200">
<Text>
This example demonstrates handling unsaved changes when closing a panel using{' '}
<code>onBeforeClose</code> with <code>@atlaskit/modal-dialog</code>.
</Text>
<Text color="color.text.subtle" size="small">
Try editing the field below, then attempt to close the panel. You'll see a modal
dialog asking if you want to discard your changes.
</Text>
<div>
<label htmlFor="form-field">
<Text as="span" size="small" weight="medium">
Field Value:
</Text>
</label>
<input
id="form-field"
type="text"
value={formValue}
onChange={(e) => {
setFormValue(e.target.value);
setHasUnsavedChanges(true);
}}
/>
</div>
{hasUnsavedChanges && (
<Text color="color.text.warning" size="small">
⚠️ You have unsaved changes. Try closing the panel to see the confirmation
modal.
</Text>
)}
</Stack>
</PanelBody>
</PanelContent>
<PanelFooter>
<ButtonGroup label="Panel actions">
<Button appearance="subtle" onClick={handleCancel}>
Cancel
</Button>
<Button appearance="primary" onClick={handleSave} isDisabled={!hasUnsavedChanges}>
Save
</Button>
</ButtonGroup>
</PanelFooter>
</PanelContainer>
</Box>
<ModalTransition>
{showDiscardModal && (
<Modal onClose={handleCancelDiscard} testId="discard-modal" width="small">
<ModalHeader hasCloseButton>
<ModalTitle appearance="danger" isMultiline={false}>
Discard your changes?
</ModalTitle>
</ModalHeader>
<ModalBody>
<Box as="p">If you leave, your changes will be lost.</Box>
</ModalBody>
<ModalFooter>
<Button appearance="subtle" onClick={handleCancelDiscard}>
Cancel
</Button>
<Button appearance="danger" onClick={handleDiscard}>
Discard changes
</Button>
</ModalFooter>
</Modal>
)}
</ModalTransition>
</>
);
}
function UnsavedChangesExample() {
return (
<Flex xcss={styles.layout}>
<Box xcss={styles.mainContent}>
<Stack space="space.300">
<Heading size="medium">Unsaved Changes Example</Heading>
<Text>
This example demonstrates handling unsaved changes when closing a panel using the{' '}
<code>onBeforeClose</code> callback.
</Text>
<Stack space="space.200">
<Heading size="small" as="h3">
How it works
</Heading>
<Text color="color.text.subtle" size="small">
The <code>onBeforeClose</code> callback is called before the panel closes. If it
returns <code>false</code> (or a Promise resolving to <code>false</code>), the close
action is prevented. This example uses <code>@atlaskit/modal-dialog</code> to show a
discard confirmation modal instead of using <code>window.confirm</code>.
</Text>
<Box xcss={styles.buttonGroup}>
<Trigger panel={panelEntryPoint} params={{}}>
{({ ref }) => (
<Button ref={ref as Ref<HTMLButtonElement>} testId="open-panel">
Open Edit Form
</Button>
)}
</Trigger>
</Box>
</Stack>
</Stack>
</Box>
<PanelsRenderer>
<UnsavedChangesPanel />
</PanelsRenderer>
</Flex>
);
}
export default function UnsavedChangesExampleWithProvider(): React.JSX.Element {
return (
<RelayEnvironmentWrapper>
<PanelProvider>
<UnsavedChangesExample />
</PanelProvider>
</RelayEnvironmentWrapper>
);
}