PageLayout
Use the PageLayout to structure app pages with persistent header, sidebar,
content, and details panel slots, including responsive breakpoint handling and
resizable panels.
Import
import { PageLayout } from '@dynatrace/strato-components/layouts';
Demo
Responsive behavior
Below its configured breakpoint, a panel renders as a Drawer instead of a
split layout, to preserve screen space. The breakpoint only selects the render
mode: it never overwrites a controlled collapsed.
Out of the box
PageLayout.Sidebar(uncontrolled): auto-collapses to a closed drawer below the breakpoint and reopens above it, so the navigation never covers the content. These auto-changes fireonCollapsedChange.PageLayout.Details: never auto-collapses.defaultCollapsed(defaultfalse) is honored on every viewport, so an open, uncontrolled details panel shows as an open drawer below the breakpoint.- Any panel: subscribe to
onBreakpointTransition(belowBreakpoint)to react to breakpoint changes. It fires on the first measurement and on every crossing, and never changes a controlledcollapsedfor you: updatecollapsedyourself if you want to.
Layout hierarchy
PageLayout.Sidebar and PageLayout.Details each have a breakpoint prop, but
they measure different widths:
PageLayout.Sidebar-breakpointis measured against the fullPageLayoutcontainer width.PageLayout.Details-breakpointis measured against the combined width of theContent+Detailsarea, to the right of the sidebar.
This means the two breakpoints fire independently. When the sidebar is expanded,
it consumes page width and shrinks the content+details area. A details panel
with breakpoint={600} may collapse to a drawer even when the full page is
wider than 600 px.
The same rule applies to width props: minWidth, maxWidth, and defaultWidth
on PageLayout.Details are percentages of the content + details area, not the
full page.
Preserve content state
Panel content is never unmounted. Component state, scroll positions, and form
input values survive collapse and expand cycles without any configuration. To
reset state on close, do so explicitly in onCollapsedChange, or conditionally
render the slot's children.
Built-in controls
Sidebar
Collapse and expand controls are always present in PageLayout.Sidebar and
require no configuration. The collapse button appears on hover when the sidebar
is expanded. When collapsed, an expand button takes its place.
Details control bar
PageLayout.Details.ControlBar is optional. Place it at the top of the details
panel to give users a close button and, above the breakpoint, a layout-mode
toggle. See Control bar in Usage for
placement and customization guidance.
Disable resizing
Set resizable={false} to lock a panel to its configured width and hide the
drag handle. Use this when the panel width is determined by its content rather
than user preference.
Programmatic width control
Pass a width prop to PageLayout.Details to take ownership of the panel
width. The panel follows the controlled value on every render; omitting width
keeps the panel self-managing via defaultWidth.
Use onResize to receive drag commits and keep the controlled value in sync.
Mode-aware clamp
Controlled widths are clamped to the same bounds that apply to dragging:
overlay: the panel can grow to fill the container, so passing'100%'expands the panel to the full available width.split: the panel is capped at the configuredmaxWidth(default'50%'), so the content area stays usable.
This keeps a programmatic width consistent with what a user can reach by dragging.
Maximize / restore button
Track a restoreWidth value to remember the pre-maximize state. A defined
restoreWidth is what counts as "maximized":
import { useState } from 'react';
import { Button } from '@dynatrace/strato-components/buttons';
import {
PageLayout,
type PanelWidth,
} from '@dynatrace/strato-components/layouts';
const DEFAULT_WIDTH: PanelWidth = '30%';
const MaximizeExample = () => {
const [width, setWidth] = useState<PanelWidth>(DEFAULT_WIDTH);
const [restoreWidth, setRestoreWidth] = useState<PanelWidth>();
const isMaximized = restoreWidth !== undefined;
const maximize = () => {
setRestoreWidth(width);
setWidth('100%');
};
const restore = () => {
setWidth(restoreWidth ?? DEFAULT_WIDTH);
setRestoreWidth(undefined);
};
return (
<PageLayout.Details
defaultLayout="overlay"
width={width}
onResize={(px) => {
setRestoreWidth(undefined);
setWidth(px);
}}
>
<PageLayout.Details.ControlBar>
<Button onClick={() => (isMaximized ? restore() : maximize())}>
{isMaximized ? 'Restore' : 'Maximize'}
</Button>
</PageLayout.Details.ControlBar>
</PageLayout.Details>
);
};
Use with router
Slot components work correctly inside a router <Outlet />, a <Suspense>
boundary, or any other wrapper, including components that conditionally render
or lazy-load their children.
A common pattern is a top-level PageLayout with the header and sidebar defined
at the app layout level, and the Content and Details slots provided by
individual route components:
// App layout - defines the persistent layout frame
const AppLayout = () => (
<PageLayout>
<PageLayout.Header>
<AppHeader>{/* top-level navigation */}</AppHeader>
</PageLayout.Header>
<PageLayout.Sidebar>{/* sub-navigation */}</PageLayout.Sidebar>
{/* Route components render their Content/Details slots here */}
<Outlet />
</PageLayout>
);
// Route component - only provides the slots it owns
const DashboardRoute = () => (
<>
<PageLayout.Content>
<DashboardTable />
</PageLayout.Content>
<PageLayout.Details>
<PageLayout.Details.ControlBar />
<DetailsPanel />
</PageLayout.Details>
</>
);
When a route lazy-loads its content, wrap it in <Suspense> and render a
fallback inside PageLayout.Content to keep the layout stable while loading:
const LazyDashboard = lazy(() => import('./DashboardRoute'));
const AppLayout = () => (
<PageLayout>
<PageLayout.Header>...</PageLayout.Header>
<PageLayout.Sidebar>...</PageLayout.Sidebar>
<Suspense
fallback={
<PageLayout.Content>
<ProgressCircle />
</PageLayout.Content>
}
>
<Outlet />
</Suspense>
</PageLayout>
);