| Section | Description |
|---|---|
| Overview | Panel as the standard desktop container with toolbars and masks |
| Basic Usage | Minimal panel examples with titles, icons, and toolbars |
| Layout | Internal vframe structure and content flow |
| contentBoxProps | Controlling the inner content frame (padding, flex, scroll) |
| scrollable | Convenience shorthand for vertical content scrolling |
| Toolbars | tbar/bbar, separators, compact mode, and overflow menus |
| Panel + Grid Pattern | Common panel-wrapping-grid structure from real apps |
| Mask | Loading overlays with TaskObserver and onLoad binding |
| Collapsing and Resizing | PanelModel config for collapse, resize, and splitters |
| collapsedTitle / collapsedIcon | Identifying collapsed panels with custom titles and icons |
| compactHeader | Reduced-size headers for visual hierarchy |
| headerItems | Inline controls in the panel header |
| Persistence | Persisting collapsed state and size across sessions |
| Modal Support | Popping panel content into a near-full-screen dialog |
| Configuration Reference | Full Panel props and PanelModel config tables |
| Common Pitfalls | mask: 'onLoad' requirements and collapsible panel tips |
| Key Source Files | Source file locations for Panel and related components |
Panel is the standard container for desktop Hoist application views. Nearly every screen in a Hoist
app is built from one or more Panels. Panel renders content in a vertical flexbox (vframe) with
optional header, top toolbar (tbar), bottom toolbar (bbar), and mask overlay.
Panels also support collapsing, resizing, and popping out to a modal dialog β all configured via
PanelModel and persistable if so configured.
import {panel} from '@xh/hoist/desktop/cmp/panel';
// Minimal panel with title and content
panel({
title: 'User Details',
icon: Icon.user(),
item: userForm()
})
// Panel wrapping a grid with toolbars
panel({
title: 'Orders',
icon: Icon.list(),
tbar: [refreshButton(), filler(), storeFilterField()],
item: grid(),
bbar: toolbar({compact: true, items: [filler(), colChooserButton(), exportButton()]})
})Panel's internal layout is a vframe (vertical flexbox). Content flows top-to-bottom:
ββββββββββββββββββββββββββββ
β Header (title + icon) β
ββββββββββββββββββββββββββββ€
β tbar β
ββββββββββββββββββββββββββββ€
β β
β children (items/item) β β contentBoxProps targets this area
β β
ββββββββββββββββββββββββββββ€
β bbar β
ββββββββββββββββββββββββββββ
When no width, height, or flex is specified, Panel defaults to flex: 'auto', allowing it to
fill available space within its parent container.
Note: Padding props (padding, paddingTop, etc.) are stripped from Panel. The header and
toolbars are designed to be flush with the panel edges β use contentBoxProps to apply padding to
the content area instead.
The contentBoxProps prop provides direct control over the inner frame that wraps Panel's children.
This frame sits between the top and bottom toolbars and receives the CSS class
xh-panel__content. It defaults to flexDirection: 'column' with flex: 'auto' and
overflow: 'hidden' (the latter two inherited from the frame() layout primitive), matching
Panel's standard vertical layout.
Use contentBoxProps to apply padding, change flex direction, enable scrolling, or add custom
classes β without introducing extra wrapper elements or CSS overrides. Pass className within
contentBoxProps to add custom CSS classes to the content frame β these are merged with the base
xh-panel__content class.
// Padded content β toolbars remain flush with panel edges
panel({
title: 'Details',
contentBoxProps: {padding: true},
item: detailForm()
})
// Horizontal content layout
panel({
title: 'Comparison',
contentBoxProps: {flexDirection: 'row', gap: 5},
items: [leftPane(), rightPane()]
})
// Scrollable content (via contentBoxProps)
panel({
title: 'Log',
contentBoxProps: {overflow: 'auto'},
item: logOutput()
})This mirrors the contentBoxProps API available on Card.
The scrollable prop is a convenience shorthand that sets overflowY: 'auto' on the content
area, keeping the header and toolbars fixed while content scrolls vertically. This is equivalent
to contentBoxProps: {overflowY: 'auto'} but reads more naturally for the common case:
// These are equivalent:
panel({scrollable: true, item: longForm()})
panel({contentBoxProps: {overflowY: 'auto'}, item: longForm()})When both scrollable and contentBoxProps.overflowY are specified, scrollable takes
precedence.
The tbar and bbar props accept either a toolbar() element or a plain array. When an array is
provided, it is automatically wrapped in a toolbar():
// These are equivalent:
panel({tbar: toolbar([refreshButton(), filler(), exportButton()])})
panel({tbar: [refreshButton(), filler(), exportButton()]})The string '-' can be used as a toolbar item to insert a visual separator. Consecutive separators
are automatically filtered out, which is useful when some controls between separators might be
omited due to user roles or other conditions:
panel({
tbar: [refreshButton(), '-', exportButton(), '-', colChooserButton()]
})Use compact: true for reduced-height toolbars. This can be used to keep them more visually lightweight, especially in space constrained layouts such as dashboard views.
panel({
bbar: toolbar({
compact: true,
items: [filler(), colChooserButton(), exportButton()]
})
})Use filler() to push subsequent items to the right side of the toolbar:
toolbar([
storeFilterField(), // left-aligned
filler(), // pushes everything after it to the right
colChooserButton(), // right-aligned
exportButton() // right-aligned
])Set enableOverflowMenu: true on a toolbar to collapse overflowing items into a dropdown menu.
This is useful for responsive layouts where toolbar width may be constrained. Only available for
horizontal toolbars.
A common pattern in Hoist apps is a panel wrapping a grid with toolbars for filtering and grid helper buttons. This example shows the typical structure drawn from real applications:
const reportPanel = hoistCmp.factory<ReportModel>({
render({model}) {
return panel({
title: 'Time Tracking',
icon: Icon.clock(),
compactHeader: true,
tbar: [
viewManager(),
groupingChooser({flex: 10, maxWidth: 350}),
filler(),
storeCountLabel({unit: 'entry'}),
'-',
storeFilterField()
],
item: grid(),
bbar: toolbar({
compact: true,
items: [
filler(),
expandToLevelButton(),
colChooserButton(),
exportButton()
]
}),
mask: 'onLoad'
});
}
});The mask prop displays a loading overlay on the panel. It accepts several forms:
| Value | Behavior |
|---|---|
'onLoad' |
Binds to the context model's loadObserver. The model must implement doLoadAsync with LoadSupport enabled. See /core/README.md for Load Support details. |
someTaskObserver |
Binds to a specific TaskObserver instance. |
[task1, task2] |
Binds to multiple TaskObserver instances β mask shows when any task is pending. |
mask({bind: ..., spinner: false}) |
Full control via the mask() component factory. |
true |
Static mask, always displayed. |
Recommended: Use mask: 'onLoad' as the default choice when the panel's linked context model
has load support. This is the simplest and most common pattern:
// Model implements doLoadAsync via @managed LoadSupport
panel({
mask: 'onLoad',
item: grid()
})
// Bind to a specific or multiple tasks for more targeted masking
panel({
mask: [model.saveTaskObserver, model.deleteTaskObserver],
item: grid()
})When 'onLoad' is specified, Panel looks up the nearest context model's loadObserver and creates
a mask with spinner: true bound to it. A warning is logged if the context model does not support
loading.
Collapse and resize behavior is configured via PanelModel, provided either as a model prop or
inline via modelConfig. When using modelConfig, Panel creates and manages the PanelModel
internally.
PanelModel requires both defaultSize and side when collapsible or resizable is true.
These tell the panel which dimension to control and which direction to collapse toward.
// Collapsible side panel (most common pattern)
panel({
title: 'Details',
compactHeader: true,
modelConfig: {
collapsible: true,
side: 'right',
defaultSize: 380
},
item: detailView()
})
// Bottom detail panel with percentage sizing
panel({
collapsedTitle: 'Details',
collapsedIcon: Icon.detail(),
compactHeader: true,
modelConfig: {
collapsible: true,
side: 'bottom',
defaultSize: '30%',
persistWith: {localStorageKey: 'myDetailPanelModel'}
},
item: dashContainer()
})
// Constrained resize range
panel({
modelConfig: {
side: 'left',
defaultSize: 300,
minSize: 200,
maxSize: 600
},
item: navTree()
})| Option | Default | Description |
|---|---|---|
collapsible |
true |
Can the panel be collapsed? |
resizable |
true |
Can the panel be resized by dragging? |
side |
β | Side toward which the panel collapses/shrinks ('top', 'bottom', 'left', 'right'). Relates to the panel's position within a parent hbox or vbox. |
defaultSize |
β | Default size in pixels (number) or percent (string, e.g. '30%'). Percentage is used for initial sizing only β once the user resizes, the size is stored in pixels (including when persisted). |
minSize |
0 |
Minimum resize size in pixels. |
maxSize |
null |
Maximum resize size in pixels. |
defaultCollapsed |
false |
Start collapsed? |
showSplitter |
resizable || collapsible |
Show a draggable splitter at the panel edge. |
showSplitterCollapseButton |
showSplitter && collapsible |
Show collapse button on the splitter. |
showHeaderCollapseButton |
true |
Show collapse button in the panel header (when collapsible). |
renderMode |
'lazy' |
How collapsed content is rendered: 'lazy' (render once, preserve), 'always', or 'unmountOnHide'. |
refreshMode |
'onShowLazy' |
How collapsed content is refreshed when re-expanded. |
resizeWhileDragging |
false |
Redraw panel continuously during resize drag. |
- Double-clicking the header toggles collapse.
- When collapsed to left or right, the header rotates vertically, showing the collapsed title/icon.
- When collapsed to top or bottom, the header remains horizontal.
- The splitter collapse button provides an additional collapse/expand affordance.
Always set collapsedTitle and collapsedIcon on collapsible panels so users can identify the
panel when collapsed. These default to the panel's title and icon but can be customized for a
more descriptive collapsed label:
panel({
title: `Time Tracking: ${model.datePeriodModel.periodDisplayName}`,
icon: Icon.clock(),
collapsedTitle: 'Details',
collapsedIcon: Icon.detail(),
compactHeader: true,
modelConfig: {collapsible: true, side: 'bottom', defaultSize: '30%'},
item: dashContainer()
})Use compactHeader: true for secondary or detail panels to create visual hierarchy. This reduces
the header's padding and font size, distinguishing it from primary panels with full-size headers:
// Primary panel β full-size header (default)
panel({
title: 'Positions Report',
icon: Icon.portfolio(),
item: hframe(
gridPanel(),
// Secondary panel β compact header
panel({
title: 'Contract Details',
compactHeader: true,
modelConfig: {collapsible: true, side: 'right', defaultSize: 400},
item: detailGrid()
})
)
})The headerItems prop places controls in the right side of the panel header, inline with the title.
This is useful for embedding lightweight status displays or contextual controls that relate to the
panel's content without taking up toolbar space:
panel({
title: 'Invoices',
icon: invoiceIcon(),
headerItems: [
relativeTimestamp({prefix: 'Refreshed', timestamp: model.lastRefreshed}),
select({bind: 'client', options: clientNames})
],
item: grid()
})Header items are hidden when the panel is collapsed β only the title, icon, and built-in collapse/modal toggle buttons are shown in the collapsed header.
PanelModel supports persisting collapsed state and size via persistWith. This uses Hoist's
standard persistence system β collapsed/expanded state and drag-resized dimensions are restored
across sessions:
panel({
modelConfig: {
side: 'bottom',
defaultSize: '30%',
persistWith: {localStorageKey: 'myDetailPanelModel'}
},
item: detailView()
})The default persistence path is 'panel'. Use the path option to disambiguate when multiple
panels persist via the same provider:
panel({
modelConfig: {
side: 'right',
defaultSize: 380,
persistWith: {...model.persistWith, path: 'commentsPanel'}
},
item: commentView()
})Panels can pop out their content into a near-full-screen dialog while fully preserving state β no re-render occurs. This is implemented via a DOM portal that moves the content between inline and modal containers.
Enable via modalSupport in modelConfig:
// Boolean β uses default 90vw x 90vh sizing
panel({
modelConfig: {
modalSupport: true,
collapsible: false,
resizable: false
},
bbar: toolbar({
compact: true,
items: [filler(), modalToggleButton()]
}),
item: chart({model: chartModel})
})
// Object config β custom sizing
panel({
modelConfig: {
modalSupport: {width: '95vw', height: '95vh'},
defaultSize: '60%',
side: 'bottom',
collapsible: false
},
item: rateSheetForm()
})modalToggleButton()β Place in a toolbar to give users a button to toggle modal state. Shows an "open external" icon inline and a "close" icon when in modal state.showModalToggleButton: true(default whenmodalSupportis enabled) β Automatically adds a toggle button to the panel header.- Double-clicking the header while in modal state closes the modal.
| Option | Default | Description |
|---|---|---|
width |
'90vw' |
Width of the modal dialog. |
height |
'90vh' |
Height of the modal dialog. |
defaultModal |
false |
Start in modal state? |
canOutsideClickClose |
true |
Close modal when clicking outside? |
Recommended for: Charts, visualizations, document previews, and any panel in space-constrained layouts like dashboards.
| Prop | Type | Description |
|---|---|---|
title |
ReactNode |
Title text in the panel header. |
icon |
ReactElement |
Icon in the panel header. |
collapsedTitle |
ReactNode |
Title shown when collapsed. Defaults to title. |
collapsedIcon |
ReactElement |
Icon shown when collapsed. Defaults to icon. |
compactHeader |
boolean |
Reduced-size header styling. |
headerItems |
ReactNode[] |
Items added to the right side of the header. |
headerClassName |
string |
CSS class for the header element. |
tbar |
ReactNode |
Top toolbar. Array auto-wrapped in toolbar(). |
bbar |
ReactNode |
Bottom toolbar. Array auto-wrapped in toolbar(). |
contentBoxProps |
BoxProps |
Props for the inner frame wrapping content items. |
scrollable |
boolean |
Allow the panel content area to scroll vertically. |
mask |
Some<TaskObserver> | ReactElement | boolean | 'onLoad' |
Mask overlay specification. |
loadingIndicator |
Some<TaskObserver> | ReactElement | boolean | 'onLoad' |
Loading indicator (same forms as mask). |
model |
PanelModel |
Explicit PanelModel for collapse/resize. |
modelConfig |
PanelConfig |
Inline config β Panel creates PanelModel internally. |
contextMenu |
ContextMenuSpec |
Right-click context menu. |
hotkeys |
HotkeyConfig[] |
Keyboard shortcuts scoped to this panel. |
See Collapsing and Resizing for the full PanelConfig options table.
Panel resolves 'onLoad' by looking up the loadObserver on the nearest context model. If that
model does not implement doLoadAsync (and therefore has no loadObserver), the mask will
silently fail β no mask will appear, and a warning will be logged to the console. Ensure the model
linked to the panel has LoadSupport enabled, or bind to a specific TaskObserver instead.
// β Don't: model has no doLoadAsync β mask: 'onLoad' will not work
panel({
mask: 'onLoad',
item: grid()
})
// β
Do: bind to a specific TaskObserver when the model lacks LoadSupport
panel({
mask: model.saveTask,
item: grid()
})A collapsible panel that has no title, collapsedTitle, or icon will not render a header
when collapsed β it collapses down to just the splitter bar. This is supported and valid, but can
make it difficult for users to locate the collapsed panel and find the small control on the
splitter to re-expand it. Setting collapsedTitle and/or collapsedIcon ensures a visible,
identifiable header is always shown when collapsed.
// β Avoid: no title or collapsedTitle β collapses to splitter only
panel({
modelConfig: {collapsible: true, side: 'right', defaultSize: 400},
item: detailView()
})
// β
Better: collapsedTitle ensures the panel is identifiable when collapsed
panel({
collapsedTitle: 'Details',
collapsedIcon: Icon.detail(),
modelConfig: {collapsible: true, side: 'right', defaultSize: 400},
item: detailView()
})| File | Description |
|---|---|
desktop/cmp/panel/Panel.ts |
Panel component β vframe layout, mask/toolbar parsing. |
desktop/cmp/panel/PanelModel.ts |
Collapse/resize state, persistence, modal support config. |
desktop/cmp/panel/impl/PanelHeader.ts |
Header rendering, collapsed title/icon switching. |
desktop/cmp/toolbar/Toolbar.ts |
Toolbar with separator shortcut, compact mode, overflow menu. |
desktop/cmp/modalsupport/ModalSupportModel.ts |
Modal pop-out config and state management. |
desktop/cmp/modalsupport/ModalSupport.ts |
Portal-based modal implementation. |
cmp/mask/Mask.ts |
Mask component with TaskObserver binding. |