electron-deck
v1.0.0
Published
Host-shell framework for Electron: multi-window orchestration, native WebContentsViews that follow the DOM, overlays, popouts and cross-process IPC behind one `electronDeck(config)` entry — plus a browser-only dock layout engine.
Maintainers
Readme
Host-shell framework for Electron: multi-window orchestration, native WebContentsViews that follow the DOM, overlays, popouts and cross-process IPC behind one
electronDeck(config)entry — plus a browser-only dock layout engine.
A framework for building a host shell on Electron out of a small set of orthogonal primitives: multi-window management, native WebContentsView stacking and geometry following, overlays and popouts, and cross-process IPC. Hosts write an injected RuntimeBackend; the framework owns Electron setup, transport wiring, and trust boundaries.
Features
- One entry point —
electronDeck(config)takes aRuntimeBackendand takes over app assembly:whenReady()gating, transport wiring, trust boundary. - DOM-following native views — WebContentsViews track DOM element geometry across the process boundary, built on top of view-anchor.
- Dockable layouts — a pure-TypeScript layout-as-data engine (
/layout) and its React renderer<DockView>(/dock-react), both usable in plain-browser projects. - Browser-pure subpaths —
/layoutand/dock-react(plus their transitive dependencies) never importelectronornode; both boundaries are pinned by tests. - Subpath exports — dedicated entry points for
main,preload,host,client,layoutanddock-react. - Graduated APIs — a stable integration path via
electronDeck(), plus an explicitly marked experimental declarative surface.
Performance
Every claim here has a command that reproduces it:
- Native views track the DOM within about 1 ms — the placement publisher coalesces every
set/removeof a render step into one IPC publish, scheduled as a post-render task instead of the next animation frame.DECK_DEMO_LAG=1 pnpm run examples:dockabledrives a real pointer drag of a split handle and reports the lag of the native view behind its DOM slot (median 0.9 ms over 152 samples here, p95 2.2 ms; forcing the same drag back ontorequestAnimationFramemoves the median to 16.9 ms — one full frame). - Tree-shaking is verified —
pnpm run check:tree-shakingverifies that importing onlycreateInitialStatefromelectron-deck/layoutshakes out its layout siblings, yielding a bundle of about 100 B gzip instead of the whole 5 KB entry; its root-entry case separately checks thatdock-reactstays outside the root dependency graph. This check runs inprepack. - Window cleanup is regression-tested —
pnpm exec vitest run src/internal/deck-app.memory-regression.test.tscycles windows and views in a fake Electron runtime, forces GC, and checks that the closed objects are collectible and heap growth stays below 1 MB. This testsDeckAppretention, not native Electron memory. - The main-process frame path is cheap —
pnpm benchmeasuresauthorizeSnapshot → reconcile → applyReconciledPlacementsper frame; expect a few microseconds per frame for a dozen views, and treat single runs as noisy.
Installation
pnpm add electron-deck
# or
npm install electron-deckelectron (>=30.5.1, the first line with WebContentsView; CI runs the e2e on both that floor and the version in devDependencies) is an optional peer dependency — browser-only consumers of /layout and /dock-react don't need it. React ≥ 18 is required for /dock-react.
Quick start
Take over app assembly
Implement a RuntimeBackend (build windows and wire your own IPC in assemble(runtime)) and hand it to electronDeck():
// main.ts
import { electronDeck } from 'electron-deck'
import { myBackend } from './my-backend.js'
electronDeck({ backend: myBackend }).catch((err) => {
console.error(err)
process.exit(1)
})The framework waits for app.whenReady(), wires the transport, and enforces the trust boundary; you only assemble your domain (real context, main window content, views, IPC modules). Set ownsWindows: true when the backend owns the main window completely.
Don't
await electronDeck()at the top level of your main module. Electron only fireswhenReadyafter the main module finishes evaluating, so a top-level await deadlocks. Use.catch(...)instead.startElectronDeck()returns its handle synchronously, so the call itself never blocks module evaluation — butawait handle.readyat the top level deadlocks identically toawait electronDeck(); defer that await into an event handler or other post-ready code.
Window-internal docking only
electron-deck/layout is a pure-TypeScript layout-as-data engine; electron-deck/dock-react is its <DockView> React renderer. Both work in plain-browser web projects.
- The layout is a serializable tree of
SplitNode/TabGroupNodenodes.movePanel,splitPanel,closePanel,insertPanel,setActive,setSizesandsetConstraintare its mutations;serializeLayout/parseLayout/collectTreeProblemshandle persistence;createLayoutModelis the observable single-writer model. - Split children can carry a
SizeConstraint:fixedPxpins a size,minPxsets a floor the user can still drag upward. - Panel descriptors can carry
PanelCapabilities(draggable/acceptsDrops/dropPolicy/closable/hideTab) that constrain dragging and closing;computeReorderIndexis the accompanying pure geometry function.
Runnable examples live in examples/layout-demo and examples/dockable-demo.
Entry points
| What you need | Import from |
| ----------------------------------------------------------- | ------------------------------------------------------ |
| electronDeck entry, DeckConfig / RuntimeBackend types | electron-deck |
| Main-process assembly utilities | electron-deck/main |
| Host-side control-bus / capability / trust primitives | electron-deck/host |
| Preload bridge exposeDeckBridge() | electron-deck/preload |
| Renderer client createDeckClient<HS, EV>() | electron-deck/client (/client/browser is an alias) |
| Layout-as-data engine + panel registry | electron-deck/layout |
| <DockView> + computeReorderIndex | electron-deck/dock-react |
Experimental: declarative assembly
A higher-level declarative surface exists via startElectronDeck() with top-level config (hostServices / events) and high-level runtime APIs (runtime.windows / runtime.view / runtime.scopes):
import { startElectronDeck, defineEvent } from 'electron-deck'
const authChanged = defineEvent<{ user: { id: string } | null }>('authChanged')
startElectronDeck({
app: { name: 'My Host' },
hostServices: { getUser: async () => ({ user: null }) },
events: [authChanged],
})It currently has no production consumers outside this repo's examples — treat it as @experimental: signatures may change and it has not been validated under non-demo workloads. Its one concrete advantage over electronDeck() is that the call itself returns synchronously, so it never blocks module evaluation — but awaiting handle.ready still has to happen after top-level evaluation finishes, same as electronDeck().
Documentation
- Architecture — the four layout / multi-window primitives, the injected
RuntimeBackend, trust boundaries, lifetimes - Connection layer —
Connection, resource ownership,debugTap - Cross-cutting contracts — capability gating and grants, unified lifetimes, view handles, view-anchor following
- Dock layout epoch and view-anchor — when dock mutations commit and when to remeasure an anchored native view
Contributing
Issues and pull requests are welcome. Before submitting, run pnpm lint, pnpm check-types, pnpm test, then pnpm build. After the build, run pnpm check:tree-shaking and pnpm check:e2e. The Electron interaction check needs a graphical session; on headless Linux, run it with xvfb-run --auto-servernum pnpm check:e2e.
License
MIT © lbb00
