-
Notifications
You must be signed in to change notification settings - Fork 41
example child windows
Stable.
A child window is a real window — own chrome, drag, resize, minimize, taskbar entry — with one rule layered on top: its owner can never sit above it. Clicking the owner shakes the child and leaves focus there.
Use it where you would otherwise reach for a modal dialog but what you actually want is a window, because the user needs to keep reading what's behind it: a full editor for one row of a list, a wizard beside the page it configures, a diff over the revision it belongs to.
The owner stays completely usable throughout — scrollable, draggable, resizable, minimizable. Only its z-order is constrained.
// Open an audit panel owned by the post window it belongs to.
const child = await wp.os.windowManager.openChild( 'edit-post-42', {
id: 'my-plugin-seo-audit-42',
url: '#seo-audit-42',
title: 'SEO audit',
icon: 'dashicons-chart-line',
width: 480,
height: 560,
native: true,
render: ( body ) => {
body.innerHTML = '<h2>Fix these before publishing</h2>';
},
} );That's it. The child opens on the owner's virtual desktop, and from then on the post window cannot be raised above it.
openChild() throws if parentWindowId names no open window — a child of nothing has nothing to block, so failing loudly beats quietly opening a standalone window.
Everything open() accepts works here too (native, render, params, initialState, …).
In precedence order:
-
Whatever you pass —
x,y,width,height. -
What the user last left it at. A child is a real window and gets the same geometry memory as any other, keyed by
baseId. Drag it aside and resize it, and that's where it comes back — not re-centered. - Centered over the owner, at 80% of the owner's current size, clamped to the desktop area.
The size defaults matter here: on the centering path, openChild() pins width and height alongside x and y. It has to — a position computed for an assumed size, applied to a window that opens at a different one, isn't centered. If you want a specific size, pass width / height and the centering follows them.
The natural trigger — a button on the owner's own title bar:
wp.os.registerTitleBarButton( {
id: 'my-plugin/seo-audit',
label: 'SEO audit',
icon: 'dashicons-chart-line',
placement: 'right',
match: ( win ) => !! win.config.url?.includes( 'post.php' ),
onClick: ( win ) => {
const id = `my-plugin-seo-audit-${ win.id }`;
// Already open? `openChild` reuses by id exactly as `open`
// does, and focus lands on the child either way.
void wp.os.windowManager.openChild( win.id, {
id,
url: `#${ id }`,
title: 'SEO audit',
icon: 'dashicons-chart-line',
native: true,
render: ( body ) => renderAudit( body, win.id ),
} );
},
} );Every blocked focus attempt fires an event. The child already shakes; this is for adding your own nudge on top.
document.addEventListener( 'os-window-child-blocked', ( e ) => {
const { windowId, childWindowId } = e.detail;
if ( childWindowId !== 'my-plugin-seo-audit-42' ) {
return;
}
wp.os.showToast( {
message: 'Finish the audit — it saves when you close it.',
} );
console.log( `${ windowId } stayed put` );
} );Same payload on the os.window.child-blocked action if you prefer the hook bus:
wp.hooks.addAction(
'os.window.child-blocked',
'my-plugin/audit-nudge',
( { windowId, childWindowId } ) => { /* … */ },
);const mgr = wp.os.windowManager;
mgr.childrenOf( 'edit-post-42' ); // [ Window ] — direct children, z-order
mgr.ownerOf( child ); // the post window
mgr.blockingChildOf( postWindow ); // the deepest child holding focus, or undefinedchildrenOf() includes minimized children (you need them to answer "what does closing this take with it"). blockingChildOf() does not — see below.
| Chains | A child can own a child. Focus goes to the deepest link; the middle ones are blocked in turn. |
| Close | Closing an owner closes its children. A child with unsaved changes still gets to ask — one that vetoes outlives its owner and becomes an ordinary window, which beats discarding the user's work. |
| Minimize | Minimizing an owner minimizes its children; restoring brings back exactly the ones the cascade put away. A child the user had already minimized themselves stays minimized. |
| Minimized children stop blocking | The user put it away on purpose, so the owner is theirs again until they bring it back. Don't build a flow that depends on the child being unreachable — it isn't a security boundary, it's an affordance. |
| Unrelated windows | Ownership constrains owner-vs-child and nothing else. Any other window can still be focused over both. |
| Session | Children are not persisted across a reload. A restored child whose owner failed to come back (deactivated plugin, dead URL) would block a window that doesn't exist. If your child holds state worth keeping, save it yourself and reopen from the owner. |
| Event cadence | A cascade emits one focus change, not one per window it moves — so os-window-focused / os-window-blurred subscribers don't see transitions the user never made. os-window-minimized / -restored still fire per window; they describe the windows, not the focus. |
If what you actually want is a visual relationship between peer windows — a post and its comments, tied together with lines drawn on the desktop, either one focusable — that's content relations, not ownership. Ownership is specifically about z-order and focus.
And if the surface really is a dialog (short, answer-then-dismiss, nothing to read behind it), use <os-confirm-dialog> / wp.os.confirm instead. A window has a title bar, a taskbar entry and a resize grip; a yes/no question doesn't need any of them.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config