Skip to content

Shell Plugin

The Shell plugin wraps the grid with an optional header bar (title + toolbar) and a collapsible tool panel sidebar. Features like visibility, filtering, and pivot register their tool panels into this shell.

import '@toolbox-web/grid/features/shell';

The feature import registers the ShellPlugin and augments features with a typed shell option. Shell configuration types (ShellConfig, ToolPanelDefinition, …) are exported from @toolbox-web/grid/plugins/shell.

Enable the shell by setting features: { shell } with a header.title. Features like visibility automatically register a tool panel when the shell is active.

import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/visibility';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
features: {
shell: { header: { title: 'Employee Data' } },
visibility: true,
},
};
Show titleShow title in shell header
Show header contentShow custom header content (row count)
Show toolbar buttonShow custom toolbar button
Visibility pluginInclude VisibilityPlugin (column chooser)
Custom tool panelShow custom tool panel via API
Panel positionTool panel position
Panel modeOverlay floats over the grid; push reflows the grid; dropdown anchors a popover to the toggle

Configure the shell declaratively using <tbw-grid-header> and <tbw-grid-header-content> elements. Still import '@toolbox-web/grid/features/shell' so the plugin is present:

<tbw-grid>
<tbw-grid-header title="Employee Directory">
<tbw-grid-header-content>
<span id="row-count">0 employees</span>
</tbw-grid-header-content>
</tbw-grid-header>
</tbw-grid>

Shell configured via <tbw-grid-header> light DOM elements — no JavaScript shell config needed.

20 employees

Register multiple tool panels — each gets a tab in the sidebar. Panels can be registered via configuration, the framework wrapper components, or the plugin’s runtime registerToolPanel() API. Each panel requires a title.

import '@toolbox-web/grid/features/shell';
const shell = grid.getPluginByName('shell');
shell?.registerToolPanel({
id: 'filter-panel',
title: 'Filters',
icon: '🔍',
render(container) {
container.innerHTML = '<p>Filter controls here</p>';
},
});

Multiple tool panels (Columns, Filter, Settings). Click toolbar icons to toggle each.

Add custom buttons to the shell toolbar using framework wrapper components or light-DOM <tbw-grid-tool-buttons>:

<tbw-grid>
<tbw-grid-tool-buttons>
<button onclick="exportData()">📥 Export</button>
<button onclick="printGrid()">🖨️ Print</button>
</tbw-grid-tool-buttons>
</tbw-grid>

Custom toolbar buttons via light DOM. The grid provides the container — you provide the HTML.

All options live under features.shell and are typed by ShellConfig (from @toolbox-web/grid/plugins/shell):

OptionTypeDefaultDescription
header.titlestring—Title text in the header bar
header.visiblebooleantrueRender the header bar. Set false to suppress the entire .tbw-shell-header and own all chrome yourself
header.toolPanelTogglebooleantrueRender the built-in panel toggle button + separator
toolPanel.position'left' | 'right''right'Sidebar position
toolPanel.widthnumber280Sidebar width in pixels
toolPanel.defaultOpenstring—Accordion section to auto-expand on first open. Also opens the sidebar on load — see #259 for the planned split. Prefer initialState / locked to control the open state and defaultOpen purely for section selection.
toolPanel.initialState'open' | 'closed''closed'Whether the sidebar starts open on grid load. Takes precedence over defaultOpen’s open-on-load behavior when set.
toolPanel.lockedbooleanfalseLock the sidebar open. Implies initialState: 'open', makes closeToolPanel() a no-op, and hides the built-in toggle button.
toolPanel.persistStatebooleanfalseRemember open/closed state
toolPanel.closeOnClickOutsidebooleanfalseClose on outside click (ignored in mode: 'push' or when locked: true; 'dropdown' always light-dismisses)
toolPanel.mode'overlay' | 'push' | 'dropdown''overlay''overlay' floats over the grid; 'push' reflows the grid sideways so all cells stay visible; 'dropdown' renders the whole sidebar as an anchored popover

The tool panel supports three layout modes — pick whichever fits the viewport you’re targeting.

  • 'overlay' (default): the panel is positioned over the grid content. Opening it does not change the grid’s available width. Best for narrow viewports where shrinking the grid would leave too little room for the data.
  • 'push': the panel is laid out as a sibling of the grid content in a flex row. Opening it shrinks the grid’s available width; the grid’s ResizeObserver re-runs column virtualization, so cells that an overlay panel would hide remain reachable. Best for desktop layouts.
  • 'dropdown': the entire sidebar (the full accordion) renders as an anchored popover built on the native Popover API. It floats above all other content in the top layer, is dismissed on Escape or click-outside, and is positioned via CSS anchor positioning where supported (with a JS bounding-rect fallback). The anchor is resolved by priority: an explicit anchor passed to openToolPanel() → the built-in toggle button → the grid corner. Best for compact toolbars and bring-your-own trigger buttons (e.g. a “columns” button in a custom column header).
import '@toolbox-web/grid/features/shell';
grid.gridConfig = {
features: {
shell: {
toolPanel: {
position: 'right',
mode: 'push', // grid reflows when the panel opens/closes
},
},
},
};

In 'push' mode the user-facing resize handle is hidden (the panel’s width is part of the layout, so drag-to-resize would fight the flex container) and closeOnClickOutside is treated as a no-op (there is no overlap to dismiss against). Drive the width via the --tbw-tool-panel-width CSS variable instead.

Section titled “Dropdown mode + a custom column-header trigger”

'dropdown' mode shines when you want the column chooser to hang off your own button rather than the built-in toolbar toggle. Pass that button as the anchor when you open the panel, and the popover positions itself directly below it:

const shell = grid.getPluginByName('shell');
// Mark your trigger so the popover can re-find it after a structural re-render.
columnsButton.setAttribute('data-panel-toggle', '');
columnsButton.addEventListener('click', () => {
shell?.openToolPanel('columns', { anchor: columnsButton });
});

The demo below renders a utility-type column whose header hosts a custom ▥ button. Clicking it opens the full tool panel as a dropdown anchored to that button — no toolbar required.

mode: 'dropdown' renders the full tool panel as an anchored popover. Here it is opened from a custom ▥ button in the header of a utility column — a bring-your-own trigger anchored via openToolPanel('columns', { anchor }).

In 'dropdown' mode the resize handle is hidden, the popover is capped to the viewport (max-height: min(70vh, 480px)) with internal scrolling for tall accordions, and dismissal (Escape / click-outside) is always active regardless of closeOnClickOutside.

<tbw-grid> renders into the light DOM, so the shell’s internals are reachable from any outer stylesheet — no ::part() needed. Theme-level overrides applied to a top-level CSS file affect every grid in the application uniformly.

Stable selectors:

SelectorElement
tbw-grid .tbw-shell-headerHeader bar (title + content + toolbar)
tbw-grid .tbw-shell-titleTitle element
tbw-grid .tbw-shell-toolbarToolbar (flex container; custom content + toggle)
tbw-grid .tbw-toolbar-btnButtons inside the toolbar (incl. panel toggle)
tbw-grid [data-panel-toggle]The single panel toggle button
tbw-grid .tbw-toolbar-separatorSeparator between custom content and the toggle
tbw-grid .tbw-tool-panelTool-panel sidebar
tbw-grid .tbw-accordion-headerTool-panel section header (one per registered panel)

Recipe — move the panel toggle to the start of the toolbar (left in LTR):

The toolbar is a flex row, so reordering is purely a CSS concern — use order on both the toggle and its separator so they stay grouped together:

/* App-level theme — affects every <tbw-grid> uniformly */
tbw-grid [data-panel-toggle],
tbw-grid .tbw-toolbar-separator {
order: -1;
}

No grid configuration is required; this is the recommended way to standardize toolbar layout across an application’s grids.

If you need to replace the built-in toggle button entirely — for example, to use a design-system button instead of styling .tbw-toolbar-btn — set header.toolPanelToggle: false. The grid will not render the built-in button or the auto-inserted separator next to it; tool panels themselves remain fully functional and can be toggled from any element via the plugin’s toggleToolPanel(), opened directly on a specific section via openToolPanel(panelId), or driven section-by-section with toggleToolPanelSection(id).

import '@toolbox-web/grid/features/shell';
const shell = grid.getPluginByName('shell');
const config = {
features: {
shell: {
header: {
title: 'Employees',
toolPanelToggle: false, // suppress built-in <button data-panel-toggle> + separator
toolbarContents: [
{
id: 'my-buttons',
render: (container) => {
const filters = document.createElement('my-button');
filters.textContent = 'Filters';
filters.addEventListener('click', () => shell?.openToolPanel('filters'));
const settings = document.createElement('my-button');
settings.textContent = 'Settings';
settings.addEventListener('click', () => shell?.openToolPanel('settings'));
container.append(filters, settings);
},
},
],
},
},
},
};

Passing a panelId to openToolPanel() jumps straight to that accordion section (one click, no double-tap). It takes precedence over toolPanel.defaultOpen; if the ID isn’t registered, the call falls back to default behavior and emits a TBW072 warning.

toolPanelToggle: false removes the built-in toggle button but keeps the header bar (with its title and any toolbar contents). To suppress the entire .tbw-shell-header bar — for example when you want to open tool panels from a custom column header icon and own all chrome yourself — set header.visible: false.

import '@toolbox-web/grid/features/shell';
const shell = grid.getPluginByName('shell');
const config = {
columns: [
// …
{
field: '__actions',
width: 56,
utility: true, // excluded from print, export, reorder, visibility, etc.
resizable: false,
sortable: false,
headerRenderer: () => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = '⚙';
btn.setAttribute('aria-label', 'Open tool panel');
btn.addEventListener('click', () => shell?.toggleToolPanel());
return btn;
},
},
],
features: {
shell: {
header: { visible: false },
toolPanel: {
closeOnClickOutside: true, // optional — see below
},
},
},
};

When header.visible: false, tool panels remain fully functional and are opened with the plugin’s toggleToolPanel() / openToolPanel(panelId) from any element you like. Because there is no header toggle to close the panel, the grid provides three additional dismissal affordances:

  • Close button — a small ✕ button (exposed as the tool-panel-close CSS part) is rendered to dismiss the panel. With a single panel it sits inline on the first accordion header row; with multiple panels it gets its own row at the top of the panel (exposed as the tool-panel-header CSS part). It is omitted when toolPanel.locked is true (a locked panel cannot be closed) and in dropdown mode, where the popover already light-dismisses on Esc / click-outside.
  • Escape key — pressing Esc closes an open overlay panel. It yields to more specific handlers (e.g. an active cell editor that has already handled the key), and is a no-op for push-mode panels, which are persistent sidebars.
  • Click-outside — set toolPanel.closeOnClickOutside: true to dismiss an open overlay panel when clicking anywhere in the window outside the panel. This is opt-in and, like Esc, is a no-op in push mode.

header.visible: false keeps the shell active (the panel overlay still mounts); it only suppresses the header bar element. This is the supported alternative to hiding .tbw-shell-header with CSS.

The shell used to be part of grid core: auto-registered with no import, driven by grid.register* / openToolPanel element delegates, with its config types exported from the package root. Those surfaces were removed in v3.0.0. The shell configuration object itself is still augmented onto gridConfig by the plugin. Migrate as follows:

Removed in v3Use instead
Implicit shell (no import)import '@toolbox-web/grid/features/shell'
grid.registerToolPanel(panel)grid.getPluginByName('shell')?.registerToolPanel(panel)
grid.unregisterToolPanel(id)grid.getPluginByName('shell')?.unregisterToolPanel(id)
grid.registerHeaderContent(c)grid.getPluginByName('shell')?.registerHeaderContent(c)
grid.registerToolbarContent(c)grid.getPluginByName('shell')?.registerToolbarContent(c)
grid.openToolPanel(id) / closeToolPanel() / toggleToolPanel()grid.getPluginByName('shell')?.openToolPanel(id) (same method names)
Shell types from @toolbox-web/gridShell types from @toolbox-web/grid/plugins/shell

Notes:

  • For advanced/explicit control you can register the plugin directly: plugins: [new ShellPlugin(shellConfig)] (from @toolbox-web/grid/plugins/shell).
  • Adapters no longer auto-register the ShellPlugin. React/Vue/Angular users must add the adapter side-effect import import '@toolbox-web/grid-{react,vue,angular}/features/shell'.
  • Shell usage without the feature/plugin enabled throws a clear error with a migration link.
  • See the v3 migration guide for the full change set.

The tool panel is resized by dragging the splitter between the panel and the grid. WCAG 2.2 SC 2.5.7 Dragging Movements requires a single-pointer alternative for every such gesture, so the splitter is a button as well as a drag target.

  • Tap the splitter — press and release without moving and the size popover opens against it, with − / a width field / + / Reset. Dragging behaves exactly as before; the two are told apart by whether the pointer moved.
  • The same popover used for columns — it is the shared control from column resizing, so the gesture, keyboard behaviour, and appearance are identical wherever something is resizable. − and + step by 32px, the field takes an exact pixel value, and Reset drops the explicit width. Escape, or a press anywhere outside, dismisses it.
  • Same limits as the drag — every path through the popover clamps to the same 200px minimum and the same available maximum, so no pointer alternative can produce a size the drag could not.
  • No reserved chrome — nothing extra is drawn. The alternative reuses the affordance you would have dragged, and the splitter carries role="button" with an accessible name instead of aria-hidden.
  • A 24px target on every pointer — SC 2.5.8 Target Size (Minimum) applies to a mouse as well as a finger, so the splitter’s invisible hit area is 24px wide while the lit bar stays 6px. It straddles the docked edge, half over the grid and half over the panel, so it never covers the panel’s own controls. Raising --tbw-touch-target-min for the rest of the grid? Raise --tbw-tool-panel-resize-overhang to half that value as well — see Hit-target sizing.

See the accessibility guide for the full inventory of drag alternatives across the grid.

The shell is unusual among plugins: most plugins enrich the grid (tagging rows, decorating cells), but the shell wraps it — it consumes the freshly built grid DOM and renders its own chrome (header bar + tool-panel sidebar) around it. It does this from the afterStructuralRender() lifecycle hook, relocating the existing .tbw-grid-content node into the shell wrapper (preserving the subtree, its listeners, and the grid’s cached refs) rather than re-creating it.

If you want to build another grid-wrapping plugin, this pattern is documented as a reusable recipe in Plugin Architecture → Wrapping plugins.

  • Filtering — Registers a filter tool panel into the shell
  • Pivot — Registers a pivot configuration tool panel
  • Core Features — Columns, renderers, formatters, events, and methods
  • Plugins Overview — Plugin compatibility and combinations