Skip to main content

Events

DockviewApi exposes events so your application can react to layout changes. All events follow the same subscription pattern and return a disposable that you should call when you no longer need the listener.

const disposable = api.onDidAddPanel((panel) => {
console.log('panel added:', panel.id);
});

// later, when cleaning up:
disposable.dispose();

Panel lifecycle

These events fire when panels are added, removed, moved, or change active state.

// Fires when a panel is added (including when panels move between groups)
api.onDidAddPanel((panel: IDockviewPanel) => {});

// Fires when a panel is removed (including when panels move between groups)
api.onDidRemovePanel((panel: IDockviewPanel) => {});

// Fires when the active panel changes. `panel` may be undefined if no panel is
// active. `origin` is 'user' for a user gesture (e.g. clicking a tab) or 'api'
// for a programmatic `setActive` call.
api.onDidActivePanelChange((event: DockviewActivePanelChangeEvent) => {
// event.panel: IDockviewPanel | undefined
// event.origin: 'user' | 'api'
});

// Fires when a panel moves from one group to another
api.onDidMovePanel((event: MovePanelEvent) => {
// event.panel: IDockviewPanel
// event.from: DockviewGroupPanel, the group the panel left
// event.to: DockviewGroupPanel, the group the panel now belongs to
});

onDidAddPanel and onDidRemovePanel fire during panel moves. A panel being moved from group A to group B will trigger a remove followed by an add. Use onDidMovePanel if you only want to react to explicit moves.

Relocation paths

onDidMovePanel covers every way a panel is relocated. Whichever path a panel takes, three things hold by the time the event reaches you:

  • it fires once the move has settled, so event.panel.api.location already reports the destination and event.to is the panel's current group;
  • the panel is back in the layout, so it appears in api.panels — you never see it mid-transition, belonging to neither group;
  • the panel's onDidLocationChange fires at most once for the whole relocation, reporting where it ended up rather than once per internal step.

What differs between paths is how many events you get, whether the panel changed group, and which transaction brackets the work:

RelocationonDidMovePanelevent.toLocation afterBracketed as
Panel dropped into another grid grouponcenew groupgridmove
Panel dragged out of an edge grouponcenew groupgridmove
Panel extracted into a floating windowoncenew groupfloatingfloat
Panel extracted into a popout windowoncenew grouppopoutpopout
Group relocated within the gridper panelsame as fromgridmove
Whole group floatedper panelsame as fromfloatingfloat
Whole group popped outper panelnew grouppopoutpopout
Group merged onto another group's centreper paneldestination groupgridmove
Popout window closedper panelreference groupgrid
Popout window closed, re-floating its groupper panelsame as fromfloatingfloat
Floating groups restored from fromJSONload
Panel added with floating: trueadd

event.to equals event.from when the group itself moved and took its panels with it. It differs when the panels were rehomed into another group — popping a group out rebuilds it in the new window, merging empties one group into another, and closing a popout returns its panels to the group it left behind in the main window.

The last two rows are not relocations at all: building a layout is not moving within one, so restoring from JSON and adding a panel straight into a floating window are silent.

Closing a popout window is currently the one path that reports moves without a transaction around them — a window closing is driven by the browser rather than by a call into the component, so nothing opens a bracket. If you are mirroring layout state, drive it from onDidMovePanel rather than from the mutation boundary alone.

Group lifecycle

These events fire when groups are added, removed, or change active state.

// Fires when a group is added (including during moves)
api.onDidAddGroup((group: DockviewGroupPanel) => {});

// Fires when a group is removed (including during moves)
api.onDidRemoveGroup((group: DockviewGroupPanel) => {});

// Fires when the active group changes. May be undefined if no group is active.
api.onDidActiveGroupChange((group: DockviewGroupPanel | undefined) => {});

Layout

These events fire when the overall layout structure changes.

// Fires on any layout change (aggregation of many events).
// Consider debouncing if using for persistence.
api.onDidLayoutChange(() => {});

// Fires after a layout is loaded via api.fromJSON()
api.onDidLayoutFromJSON(() => {});

// Fires when a group is maximized or un-maximized
api.onDidMaximizedGroupChange((event: DockviewMaximizedGroupChangeEvent) => {});

Mutation boundary

onWillMutateLayout and onDidMutateLayout bracket each top-level structural change to the layout. Unlike onDidLayoutChange (which is coalesced and only fires after a change), these provide an explicit before/after pair, so you can capture a snapshot of the layout before the mutation runs. This makes them the right hook for autosave, dirty-tracking and undo/redo.

// Fires immediately before a structural mutation
api.onWillMutateLayout((event: DockviewLayoutMutationEvent) => {
// event.kind: 'add' | 'remove' | 'move' | 'float' | 'popout'
// | 'tab-group' | 'load' | 'clear'
// event.origin: 'user' | 'api'
const snapshot = api.toJSON(); // pre-image, before the change applies
});

// Fires immediately after the same mutation settles
api.onDidMutateLayout((event: DockviewLayoutMutationEvent) => {});

A compound operation brackets as a single transaction: dragging a panel to a new group, or restoring a whole layout via fromJSON, fires exactly one before/after pair rather than one per internal step. addPopoutGroup is asynchronous - it resolves once its window has opened - and its transaction stays open for the whole operation, so the panel moves it makes are bracketed like those of every other relocation.

event.origin distinguishes who caused the change: 'user' for direct interaction (drag-and-drop, tab UI), 'api' for a programmatic DockviewApi call made by your own code. Consumers such as an undo stack can use this to ignore the app's own programmatic mutations.

Drag and drop

These events let you intercept and customise drag and drop behaviour.

// Fires before dockview handles a drop. Call event.preventDefault() to cancel it.
api.onWillDrop((event: DockviewWillDropEvent) => {
// event.preventDefault() to cancel the drop
});

// Fires when a drop completes that dockview handled
api.onDidDrop((event: DockviewDidDropEvent) => {});

// Fires before a drop overlay is shown. Call event.preventDefault() to prevent it.
api.onWillShowOverlay((event: DockviewWillShowOverlayLocationEvent) => {
// event.preventDefault() to hide the overlay for this position
});

// Fires before a panel tab drag begins. Call event.nativeEvent.preventDefault() to cancel.
api.onWillDragPanel((event: TabDragEvent) => {});

// Fires before a group header drag begins. Call event.nativeEvent.preventDefault() to cancel.
api.onWillDragGroup((event: GroupDragEvent) => {});

// Fires for drag-over events that dockview did not originate.
// Call event.accept() to let dockview show a drop overlay for external drags.
api.onUnhandledDragOver((event: DockviewDndOverlayEvent) => {
event.accept();
});

See Drag and drop and External Dnd Events for full examples.

Popout window

These events fire in response to popout window activity.

// Fires when a popout group opens in its own window, carrying the live Window
// handle. Use it to route focus or attach listeners to the popout's document.
api.onDidAddPopoutGroup((event: PopoutGroup) => {
// event.id, event.group, event.window
event.window.focus();
});

// Fires when a popout group is removed: the user closed its window or it was
// docked back programmatically. Not fired during component disposal.
api.onDidRemovePopoutGroup((event: PopoutGroup) => {
// event.id, event.group
});

// Fires when a popout window is resized
api.onDidPopoutGroupSizeChange((event: PopoutGroupChangeSizeEvent) => {});

// Fires when a popout window is repositioned
api.onDidPopoutGroupPositionChange(
(event: PopoutGroupChangePositionEvent) => {}
);

// Fires when the browser blocked opening a popout window (e.g. popup blocker)
api.onDidOpenPopoutWindowFail(() => {
console.warn('popup was blocked by the browser');
});

Enumerate the popout groups currently open at any time:

for (const popout of api.getPopouts()) {
// popout.id, popout.group, popout.window
}

See Popout Windows for more.

Tab groups

These events fire when tab groups are created, destroyed, or modified across any group.

// Fires when a tab group is created
api.onDidCreateTabGroup((event: DockviewTabGroupChangeEvent) => {
console.log('tab group created:', event.tabGroup.id);
});

// Fires when a tab group is destroyed (including auto-destroy when emptied)
api.onDidDestroyTabGroup((event: DockviewTabGroupChangeEvent) => {});

// Fires when a panel is added to a tab group
api.onDidAddPanelToTabGroup((event: DockviewTabGroupPanelChangeEvent) => {
console.log(`panel ${event.panelId} added to group ${event.tabGroup.id}`);
});

// Fires when a panel is removed from a tab group
api.onDidRemovePanelFromTabGroup(
(event: DockviewTabGroupPanelChangeEvent) => {}
);

// Fires when a tab group's properties change (label, color)
api.onDidTabGroupChange((event: DockviewTabGroupChangeEvent) => {});

// Fires when a tab group is collapsed or expanded
api.onDidTabGroupCollapsedChange(
(event: DockviewTabGroupCollapsedChangeEvent) => {
console.log(
event.tabGroup.id,
event.tabGroup.collapsed ? 'collapsed' : 'expanded'
);
}
);

See Tab Groups for the full API.

Panel API events

Individual panels also expose events via panel.api:

// Fires when the panel's title changes
panel.api.onDidTitleChange(({ title }) => {});

// Fires when the panel becomes visible or hidden
panel.api.onDidVisibilityChange(({ isVisible }) => {});

// Fires when the panel moves to a different group
panel.api.onDidGroupChange(() => {});

// Fires when the panel's group active state changes
panel.api.onDidActiveGroupChange(({ isActive }) => {});

// Fires when the panel's location changes (grid → floating → popout). Fires
// once per relocation, reporting where the panel settled - see the relocation
// table above. It also fires when the location `type` is unchanged but the
// window is not, such as a panel moving between two floating or popout
// windows, so read `location` rather than assuming the type differs from the
// last time you were called.
panel.api.onDidLocationChange(({ location }) => {});

// Fires when the panel's renderer mode changes
panel.api.onDidRendererChange(({ renderer }) => {});

Group API events

Groups expose events via group.api:

// Fires when the active panel within this group changes
group.api.onDidActivePanelChange((event) => {
// event.panel: IDockviewPanel
// event.origin: 'user' | 'api'
});

// Fires when the group location changes (grid → floating → popout)
group.api.onDidLocationChange(({ location }) => {});

There are two onDidActivePanelChange events that differ in scope:

  • api.onDidActivePanelChange (DockviewActivePanelChangeEvent) tracks the active panel across the whole Dockview; panel may be undefined.
  • group.api.onDidActivePanelChange (DockviewGroupActivePanelChangeEvent) is scoped to a single group.

Both carry an origin ('user' | 'api') reporting whether the change came from a user gesture or an API call.