Skip to content

Modals & Side Panels ​

react-dockable-desktop includes a fully integrated overlay system: a modal stack, a left drawer, and a right drawer. All three use the same dirty-state and close-guard hooks as regular panels (usePanel().setDirty, useBeforeClose) — with one difference: in a modal or drawer, a close guard that returns true closes it without the dirty-state dialog.

Setup ​

The overlay system is part of DockableDesktopProvider. You only place the two renderer components in the correct positions in your tree:

tsx
// App.tsx
import {
  DockableDesktopProvider,
  RddDesktop,
  RddModals,
  RddSidePanels,
} from 'react-dockable-desktop';

export default function App() {
  return (
    <DockableDesktopProvider workspace={workspace}>
      <div className="rdd-fill-viewport" style={{ position: 'relative' }}>
        <RddDesktop />
        <RddSidePanels />  {/* inside the sized container — used for positioning drawers */}
      </div>
      <RddModals />      {/* outside the sized container — full-screen overlay */}
    </DockableDesktopProvider>
  );
}

Placement matters

  • RddSidePanels must be a sibling of RddDesktop, inside the positioned container. Drawers position themselves relative to this container.
  • RddModals must be outside that container so modals can overlay the entire viewport.

RddSidePanels renders both drawers. To render only one, pass side="left" or side="right" — for example to place each drawer in a different container. defaultWidth sets the width used when an openLeft/openRight call doesn't give one.

useModals() and useSidePanels() ​

Overlay operations go through two hooks, available in any component inside the provider — useModals() for the modal stack and useSidePanels() for the drawers:

ts
import { useModals, useSidePanels } from 'react-dockable-desktop';

function MyComponent() {
  const modals = useModals();          // open, close, closeAll, get, update, setDirty, stack, topmost
  const sidePanels = useSidePanels();  // openLeft, openRight, close, closeAll, get, update, setDirty, left, right
}

Opening a modal ​

ts
const id = modals.open(Component, props, options?);

open pushes a new modal onto the stack and returns the instance ID. The modal appears on top of the workspace.

tsx
function LaunchButton() {
  const modals = useModals();

  const handleClick = () => {
    const id = modals.open(SettingsPanel, { section: 'general' }, {
      title: 'Settings',
      size:  'large',
    });
    // id can be used later: modals.close(id), modals.get(id), etc.
  };

  return <button onClick={handleClick}>Settings</button>;
}

ModalOptions ​

OptionTypeDefaultDescription
titlePanelTitle—Modal header title: a string, a message descriptor ({ id, defaultMessage }) or a function returning a string.
iconReactNode—Icon displayed in the title bar. (The icon beside an RddConfirm or RddAlert message is the dialog's own icon prop.)
size'small' | 'medium' | 'large' | 'fullscreen' | 'auto''auto' (sized to its content)Controls max-width of the modal.
closablebooleantrueWhen false, hides the × button and disables backdrop click-to-close.
bodyPaddingnumber | string0CSS padding for the modal body content. Numbers are treated as pixels; strings as any CSS value/shorthand (e.g. '10px 16px'). Default is edge-to-edge — pass 10 to restore the pre-v6.0.0 default.

Opening a side drawer ​

Drawers slide in from the left or right edge of the workspace container.

ts
const id = await sidePanels.openLeft(Component, props, options?);
const id = await sidePanels.openRight(Component, props, options?);
tsx
const sidePanels = useSidePanels();

const showDetails = async () => {
  const id = await sidePanels.openRight(DetailsPanel, { itemId: 'abc' }, {
    title: 'Item Details',
    width: 380,
  });
};

SidePanelOptions ​

OptionTypeDefaultDescription
titlePanelTitle—Drawer header title: a string, a message descriptor or a function returning a string.
iconReactNode—Icon next to the title.
widthnumber | string400 (px)Drawer width. Numbers are treated as pixels; strings as CSS values (e.g. '40%').
bodyPaddingnumber | string0CSS padding for the panel body content. Numbers are treated as pixels; strings as any CSS value/shorthand (e.g. '10px 16px'). Default is edge-to-edge — pass 10 to restore the pre-v6.0.0 default.

Closing panels ​

ts
// Close one instance by ID, on the hook for its kind:
modals.close(id);
sidePanels.close(id);

// Close all modals, leave drawers open:
modals.closeAll();

// Close both drawers:
sidePanels.closeAll();

The open overlays are on the hooks too: modals.stack (bottom to top), modals.topmost, and sidePanels.left / sidePanels.right.

From inside the overlay's own component, use usePanel().close() instead:

ts
const panel = usePanel();
panel.close();                  // respects dirty-state guard
panel.close({ force: true });   // bypasses all guards

Dirty state in modals ​

The same dirty-state mechanism works inside modals. Call panel.setDirty(true) inside your modal component and the user will see the confirmation dialog before the modal closes. useBeforeClose() works in modals and drawers too:

tsx
function EditModal() {
  const panel = usePanel();
  const [saved, setSaved] = useState(false);

  const handleInput = () => panel.setDirty(true);
  const handleSave  = () => { save(); setSaved(true); panel.setDirty(false); };

  return (
    <div>
      <input onChange={handleInput} />
      <button onClick={handleSave}>Save</button>
    </div>
  );
}

Stacking modals ​

Multiple modals.open calls stack visually. The topmost modal is active; pressing ESC or clicking the backdrop closes only the topmost.

One ESC closes one overlay — the one on top. Context menus and toolbar flyouts come first, then modals (topmost first), then side drawers (the one opened last first). A modal with closable: false swallows ESC rather than letting it close the drawer behind it. A control inside an overlay that handles ESC itself — a search box clearing its query, say — can keep the overlay open by calling event.preventDefault() in its own onKeyDown.

ts
const id1 = modals.open(StepOneModal, {});
// User action opens a second modal on top:
modals.open(RddConfirm, {
  message: 'Continue to step 2?',
  onOK:    () => advance(),   // RddConfirm closes itself after onOK / onCancel
  onCancel: () => {},
});

RddConfirm — built-in yes/no dialog ​

Import and use RddConfirm directly in modals.open for quick confirmations without writing a custom component:

tsx
import { RddConfirm, useModals } from 'react-dockable-desktop';

const modals = useModals();

const confirm = () => {
  modals.open(RddConfirm, {
    title:    'Delete item',
    message:  'This will permanently delete the item.',
    alert:    'This cannot be undone.',
    alertType: 'danger',
    useYesNoTitles: true,
    onOK:    () => deleteItem(),   // it closes itself after either button
    onCancel: () => {},
  });
};

A question icon sits left of the message, coloured by alertType. Pass icon to draw your own node in its place, or icon: null for none. The modal's own header icon (ModalOptions.icon) is separate and left as you set it.

onOK and onCancel fire for their buttons only. To learn how the dialog ended — including Escape, the backdrop, the × or a close by code — pass onSettled: it fires exactly once, with true for the confirm button and false for everything else.

PropTypeDefaultDescription
messagestring | MessageDescriptor—The question.
titlePanelTitlemodalTitle messageHeader title.
alertstring—An extra notice shown above the message.
alertType'info' | 'warning' | 'success' | 'danger''info'Colours the notice and the icon.
iconReactNode | nullquestion iconThe icon left of the message; null hides it.
useYesNoTitlesbooleanfalseYes / No instead of OK / Cancel.
onOK / onCancel() => void—The confirm / cancel button.
onSettled(ok: boolean) => void—Once, on every exit.

Its buttons carry data-rdd-confirm-ok and data-rdd-confirm-cancel for tests.

See Panel Lifecycle & Forms → for how the dirty-state dialog uses it; its props type is RddConfirmProps.

RddAlert — built-in message dialog ​

RddAlert shows a message and a single OK button: no question, nothing to choose. The icon left of the message follows alertType (info, success, warning, danger); icon replaces it and icon: null hides it, as for RddConfirm.

tsx
import { RddAlert, useModals } from 'react-dockable-desktop';

modals.open(RddAlert, {
  title:     'Export finished',
  message:   'The layer was exported to exports/roads.geojson.',
  alertType: 'success',
  onSettled: () => {},   // once, however it is closed
});
PropTypeDefaultDescription
messagestring | MessageDescriptor—The text.
titlePanelTitle—Header title.
alertType'info' | 'warning' | 'success' | 'danger''info'Picks the built-in icon and its colour.
iconReactNode | nullthe type's iconnull hides it.
okLabelstring | MessageDescriptorok messageThe button label.
onSettled() => void—Once: OK, Enter, Escape, the backdrop, the × or a close by code.

Focus starts on OK, and Enter presses it. Escape, the backdrop and the × acknowledge it too, unless the modal is opened with closable: false — then OK is the only way out. The button carries data-rdd-alert-ok.

modals.confirm() / modals.alert() — as promises ​

For the common case, useModals() opens either dialog and hands back a promise:

ts
const modals = useModals();

if (await modals.confirm({ message: 'Delete the selected features?', alertType: 'danger', yesNo: true })) {
  deleteSelection();
}

await modals.alert({ message: 'Nothing to export.', alertType: 'warning' });
  • confirm(options) resolves true for the confirm button, false for cancel and for any dismissal. Options: message, title (default: the modalTitle message, "Confirmation"), alert, alertType, icon, yesNo, size (default 'small').
  • alert(options) resolves once it is closed, however. Options: message, title (default: the alertTitle message, "Information"), alertType, icon, okLabel, size (default 'small').

The option types are ConfirmOptions and AlertOptions.

RddSidebar component ​

RddSidebar is a composite layout component that renders a vertical tab strip and a collapsible drawer panel. It handles all open/close animation, keyboard navigation, and state preservation internally.

tsx
import { RddSidebar, type SidebarHandle } from 'react-dockable-desktop';
import { useRef } from 'react';

const sidebarRef = useRef<SidebarHandle>(null);

<RddSidebar
  ref={sidebarRef}
  position="right"
  defaultWidth={280}
  tabs={[
    {
      id: 'layers',
      label: 'Layers',
      icon: <LayersIcon />,
      renderContent: (tabId, onClose, onOpen) => (
        <LayerTree onLayerSelect={() => onOpen()} />
      ),
    },
    {
      id: 'properties',
      label: 'Properties',
      icon: <SettingsIcon />,
      preserveState: true,           // keep alive when not visible
      renderContent: () => <PropertiesPanel />,
    },
  ]}
>
  <MainMapArea />   {/* rendered in the space beside the sidebar */}
</RddSidebar>

SidebarTab ​

PropTypeRequiredDescription
idstring✓Unique key for this tab.
labelstring✓Tooltip / accessible label for the tab icon button.
iconReactNode—Icon displayed in the tab strip. Required unless hidden is true. Pass it no size: rail icons follow --rdd-chrome-icon-size (22px).
renderContent(tabId, onClose, onOpen) => ReactNode✓Returns the drawer content. onClose collapses the drawer; onOpen expands it to this tab.
eagerMountboolean—Mount immediately on sidebar render (before the user clicks). Implies preserveState: true. Use when other parts of the app need to interact with the panel before the user opens it.
preserveStateboolean—Keep the component alive in the DOM behind display: none when closed, instead of unmounting it.
hiddenboolean—Omit this tab's rail button entirely — no icon, no click target — while it stays fully openable via openTab()/useSidebar().openTab()/a controlled activeTabId. Use for menu-driven panels with no persistent icon (e.g. a Google-Maps-style hamburger that opens content not otherwise pinned to the rail). Default false.

RddSidebarProps ​

PropTypeDefaultDescription
tabsSidebarTab[]—Required. Tab definitions.
headerActionan entry or an array of entries — each a SidebarActionButton, SidebarCustomEntry or SidebarTab—One or more non-toggling action buttons and/or real tabs shown above the tabs. See headerAction/footerAction below.
footerActionsame as headerAction—Mirror of headerAction, pinned to the bottom of the tab strip regardless of tab count. See headerAction/footerAction below.
showCloseButtonbooleanfalseShow an "X" close button in the expanded drawer's header — an extra way to collapse the sidebar besides clicking the active tab's own icon again. Has no effect once the default header is suppressed — via hideDefaultHeader, or simply by passing renderHeader (either one is sufficient) — since the entire default header, this button included, is skipped for every tab in that case.
hideDefaultHeaderbooleanfalseSuppress the library's own drawer header (title + showCloseButton's close button) for every tab — not per-tab — so renderHeader (or each tab's own renderContent) can supply a header, border, and styling instead. Passing renderHeader by itself has the same suppressing effect even if this is left unset — the two props are combined with OR, so supplying renderHeader alone is never a silent no-op. The close mechanism works the same either way: the onClose parameter passed to renderContent/renderHeader, or useSidebarTab().onClose from anywhere in a tab's content tree.
renderHeader(tab: SidebarTab, onClose, onOpen) => ReactNode—Custom header renderer used in place of the library's own drawer header. Passing renderHeader is by itself sufficient to suppress the default header, whether or not hideDefaultHeader is also set. Called once for whichever tab is currently active, so one header implementation is shared uniformly across every tab instead of being repeated inside each tab's renderContent. Omit renderHeader and set hideDefaultHeader: true to render no header at all.
position'left' | 'right''right'Side the tab strip and drawer appear on.
defaultWidthnumber280Initial drawer width in pixels.
minWidthnumber150Minimum drawer width in pixels during drag-resize.
maxWidthnumber600Maximum drawer width in pixels during drag-resize.
onWidthChange(px: number) => void—Called during drag-resize and when setWidth() is invoked.
activeTabIdstring | null—Controlled active tab. Use with onActiveTabChange for fully-controlled mode.
onActiveTabChange(tabId: string | null) => void—Called when the active tab changes.
visiblebooleantrueCollapse the entire sidebar (strip + drawer) to zero width via CSS transition. State is preserved — no unmount.
onVisibilityChange(visible: boolean) => void—Called when show/hide/toggle is invoked on the imperative handle. Wire to your useState setter.
stripVisiblebooleantrueCollapse only the activity bar strip, leaving the drawer unaffected.
onStripVisibilityChange(visible: boolean) => void—Called when showStrip/hideStrip is invoked on the imperative handle.
childrenReactNode—Main content (rendered in the area beside the sidebar).

Active tab styling — The visual treatment of the active tab button (shape, fill, indicator) is controlled entirely by CSS design tokens and varies per skin. vscode uses a transparent fill with a 2 px accent bar; macos renders a floating glass chip; nord draws a short horizontal line below the icon. See Per-skin active state design language → to customise this in your own skin.

Dual sidebars ​

RddSecondarySidebar is a second, independent RddSidebar instance for the opposite edge of the screen — same component, same behavior, no forked implementation. It must be rendered inside a primary RddSidebar's children (this is a hard requirement, not just a recommendation — it detects the primary via context, which only flows to descendants) and automatically takes whichever side the primary isn't using, so you never specify a side yourself:

tsx
import { RddSidebar, RddSecondarySidebar } from 'react-dockable-desktop';

<RddSidebar position="left" tabs={primaryTabs}>
  <RddSecondarySidebar tabs={secondaryTabs}>
    {/* Your app content */}
  </RddSecondarySidebar>
</RddSidebar>

RddSecondarySidebarProps is identical to RddSidebarProps except position isn't settable (it's always the opposite of the primary) — everything else (tabs, headerAction/footerAction, hideDefaultHeader/renderHeader, controlled activeTabId, showCloseButton, and so on) works exactly the same as on a primary RddSidebar.

WARNING

RddSecondarySidebar throws if rendered without a primary RddSidebar ancestor, or if nested inside another RddSecondarySidebar — this library supports exactly one primary and one secondary sidebar, nothing deeper.

Reach either sidebar's actions from anywhere in its own tree via useSidebar() — position/isSecondary on its return value (see useSidebar() below) tell you which one you're inside. Content nested inside the secondary that needs to control the primary (or vice versa) needs a ref/SidebarHandle passed down explicitly — useSidebar() always resolves to the nearest instance, not a specific one.

headerAction/footerAction ​

One or more non-toggling action buttons — a hamburger menu, for example — shown above (headerAction) or pinned below (footerAction) the tabs. Pass a single object (the common case) or an array to show several. An array can also mix in real SidebarTab entries — a tab placed in headerAction/ footerAction behaves exactly like a main-list tab (it mounts, activates, renders its drawer content, and closes through the same lifecycle); only its position in the rail differs. A classic use case: end footerAction with a "Settings" tab that expands like any other tab, preceded by one or more simple action buttons.

Action button entries never affect activeTabId or the drawer: RddSidebar only renders them and forwards the click. What happens next (opening a side panel, a modal, a custom menu, or nothing at all) is entirely up to you.

tsx
<RddSidebar
  tabs={tabs}
  headerAction={{
    icon: <MenuIcon />,
    label: 'Menu',
    onClick: () => sidePanels.openLeft(MainMenu, {}, { title: 'Menu' }),
  }}
  footerAction={[
    { icon: <InfoIcon />, label: 'About', onClick: () => modals.open(About, {}) },
    {
      id: 'settings',
      label: 'Settings',
      icon: <SettingsIcon />,
      renderContent: () => <SettingsPanel />,
    },
  ]}
>
  <MainMapArea />
</RddSidebar>

Each entry takes one of three forms:

FormShapeDescription
Default button{ icon, label, onClick, disabled? }Renders a button visually consistent with the regular tab buttons — same styling, current skin, aria-label (not aria-pressed, since it's never in a pressed state).
Fully custom{ render: () => ReactNode }Renders exactly what you return, with no wrapping element — a Material UI IconButton, a Bootstrap Button, a Tailwind-styled <button>, or anything else keeps its own hover/active/focus/ripple behavior and click handling completely untouched.
Real tabSidebarTab ({ id, label, icon?, renderContent, hidden?, ... })Behaves exactly like an entry in tabs — toggles active/inactive, opens the drawer, participates in eagerMount/preserveState, and is subject to the same auto-close-when-removed guard. hidden: true works here too — e.g. a menu-driven entry with no rail button, opened only via openTab().
tsx
// Fully custom — a Bootstrap button, unmodified:
<RddSidebar
  tabs={tabs}
  headerAction={{
    render: () => (
      <button type="button" className="btn btn-outline-secondary" onClick={openMenu}>
        <MenuIcon />
      </button>
    ),
  }}
/>

headerAction renders inside its own .rdd-sidebar-header-area; footerAction renders inside a mirror .rdd-sidebar-footer-area, pushed to the bottom of the strip regardless of tab count. Both are independent of the tabs' own inter-item spacing (owned by a separate .rdd-sidebar-tabs-list wrapper). Override --rdd-sidebar-header-area-padding-top/-bottom and --rdd-sidebar-footer-area-padding-top/-bottom (all default 8px) to adjust their spacing — and, by extension, their effective height, since height is just padding plus whatever you render.

SidebarHandle imperative ref ​

Obtain with useRef<SidebarHandle>():

ts
// Open a specific tab programmatically (e.g. when new data arrives):
sidebarRef.current?.openTab('layers');

// Collapse the drawer:
sidebarRef.current?.closeDrawer();

// Query current state:
const activeTab = sidebarRef.current?.getActiveTab();  // → string | null
MethodReturnsDescription
openTab(tabId)voidExpand drawer and activate the specified tab.
closeDrawer()voidCollapse the drawer.
getActiveTab()string | nullCurrently active tab ID, or null if collapsed.
show()voidShow the entire sidebar (calls onVisibilityChange(true)).
hide()voidHide the entire sidebar (calls onVisibilityChange(false)).
toggle()voidToggle sidebar visibility.
showStrip()voidShow only the activity bar strip (calls onStripVisibilityChange(true)).
hideStrip()voidHide only the activity bar strip (calls onStripVisibilityChange(false)).
setWidth(px)voidProgrammatically set the drawer width in pixels (respects minWidth/maxWidth).
getWidth()numberReturns the current drawer width in pixels.

Two hooks let panels control the sidebar without a ref or prop drilling.

useSidebar() ​

Available to any component inside an <RddSidebar> tree — including floating panels and docked panels rendered via {children}:

tsx
import { useSidebar } from 'react-dockable-desktop';

function LayerTree() {
  const { openTab, closeDrawer, getActiveTab } = useSidebar();

  return (
    <button onClick={() => openTab('search')}>
      Show Search Results
    </button>
  );
}
ValueTypeDescription
openTab(tabId)(tabId: string) => voidExpand the drawer and activate the given tab.
closeDrawer()() => voidCollapse the drawer.
getActiveTab()() => string | nullReturns the current tab ID, or null if collapsed.
position'left' | 'right'Which side this sidebar instance is rendering on.
isSecondarybooleantrue if this instance is an RddSecondarySidebar, false for a primary RddSidebar.

WARNING

useSidebar() throws if called outside an <RddSidebar> tree. A reusable panel component that may render with or without a surrounding sidebar should check for that possibility itself (e.g. a prop indicating whether one is present) rather than relying on this hook to degrade gracefully.

useSidebarTab() ​

Available to components rendered inside a tab's renderContent tree. Provides both self-control and cross-tab navigation:

tsx
import { useSidebarTab } from 'react-dockable-desktop';

function SearchResultsPanel() {
  const { tabId, onOpen, onClose, openTab } = useSidebarTab();

  return (
    <div>
      <button onClick={onClose}>Collapse</button>
      <button onClick={() => openTab('settings')}>Open Settings</button>
    </div>
  );
}

// In your tab config — the content must be a React component, not an inline arrow function,
// because useSidebarTab() uses React hooks internally:
{
  id: 'search',
  label: 'Search Results',
  icon: <SearchIcon />,
  renderContent: () => <SearchResultsPanel />,
}
ValueTypeDescription
tabIdstringThe ID of this tab.
onOpen()() => voidExpand the drawer and activate this tab.
onClose()() => voidCollapse the drawer.
openTab(tabId)(tabId: string) => voidSwitch to a different tab.

Cross-panel pattern: floating window → sidebar tab ​

A floating panel can open a sidebar tab and broadcast data in a single action:

tsx
import { useSidebar, useWorkspace } from 'react-dockable-desktop';

function LayerTree() {
  const { openTab } = useSidebar();
  const { publish } = useWorkspace();

  const handleSearch = (query: string) => {
    const results = performSearch(query);
    publish('search:results', results);   // SearchResultsPanel subscribes to this
    openTab('search');                    // expand sidebar to show it
  };

  // ...
}

For the reactive variant — where the sidebar tab opens itself when it receives data — see Event Bus & Communication →.

Opening a tab in response to data ​

Use eagerMount + onOpen when a background process needs to surface data in the sidebar before the user has clicked:

tsx
{
  id: 'alerts',
  label: 'Alerts',
  icon: <AlertIcon />,
  eagerMount: true,   // mount immediately so the panel can receive events
  renderContent: (tabId, onClose, onOpen) => (
    <AlertsPanel
      onNewAlert={() => onOpen()}  // expand sidebar when a new alert arrives
    />
  ),
}

usePanelContextMenu() hook ​

Inject custom items into a panel's right-click context menu from inside the panel component. The hook reads the panel ID internally — no prop needed. Items are dynamic: the array is re-read every time the menu opens, so state-driven changes (enable/disable, add/remove) take effect automatically.

tsx
import { usePanelContextMenu } from 'react-dockable-desktop';

function EditorPanel() {
  const [dirty, setDirty] = useState(false);

  usePanelContextMenu([
    { label: 'Save',   action: () => save(),   disabled: !dirty },
    { label: 'Revert', action: () => revert(), disabled: !dirty },
    { separator: true },
    { label: 'Copy Panel Link', action: () => copyLink() },
  ]);

  return <Editor onChange={() => setDirty(true)} />;
}

The items array accepts ContextMenuItem entries exported from react-dockable-desktop:

ShapeDescription
{ label, icon?, action, title? }A clickable menu item.
{ separator: true }A visual divider.
{ label, items: [...] }A sub-menu (one level deep).
{ label, checkbox: { active?, enabled, value }, action }A checkbox item.

See the Context Menus guide for the full type reference.

TIP

usePanelContextMenu is safe to call unconditionally — it is a no-op when the component renders outside a DockableDesktopProvider (e.g., in tests).

ModalsApi / SidePanelsApi reference ​

ts
interface ModalsApi {
  stack: OverlayInstance[];                 // open modals, bottom to top
  topmost: OverlayInstance | null;
  open<P>(Component: ComponentType<P>, props: P, options?: ModalOptions): OverlayId;
  close(id: OverlayId): void;
  closeAll(): void;
  get(id: OverlayId): OverlayInstance | undefined;
  update(id: OverlayId, updates: { props?, options?, dirty?, dirtyOptions? }): void;
  setDirty(id: OverlayId, dirty: boolean, options?: DirtyStateOptions): void;
  confirm(options: ConfirmOptions): Promise<boolean>;  // false on cancel or dismissal
  alert(options: AlertOptions): Promise<void>;
}

interface SidePanelsApi {
  left: OverlayInstance | null;
  right: OverlayInstance | null;
  openLeft<P>(Component: ComponentType<P>, props: P, options?: SidePanelOptions): Promise<OverlayId | null>;
  openRight<P>(Component: ComponentType<P>, props: P, options?: SidePanelOptions): Promise<OverlayId | null>;
  close(id: OverlayId): void;
  closeAll(): void;                         // closes both drawers
  get(id: OverlayId): OverlayInstance | undefined;
  update(id: OverlayId, updates: { props?, options?, dirty?, dirtyOptions? }): void;
  setDirty(id: OverlayId, dirty: boolean, options?: DirtyStateOptions): void;
}

See also ​

Released under the MIT License.