Repository navigation
example window controls
The title-bar control cluster (close / minimize / maximize / focus) is rendered from a registry plugins can extend, reorder, hide, or replace per-window. Built-in controls live in the same registry as plugin controls, addressed by the stable ids core/minimize, core/maximize, core/focus-tab, core/close. (Detach and reload moved into the title-bar three-dots menu and are no longer registry controls.)
This is Layer 2 of the four-layer window-chrome customization framework. See Window themes for Layer 1.
Move the close button to the leftmost position on a specific native window:
wp.os.registerWindow( {
id: 'my-plugin/dashboard',
title: 'Dashboard',
icon: 'dashicons-dashboard',
width: 640, height: 480,
minWidth: 320, minHeight: 200,
appearance: {
controls: {
order: [ 'core/close', 'core/minimize', 'core/maximize' ],
},
},
render: ( body ) => { body.textContent = 'Hello'; },
} );Controls listed in order render in that order; controls not listed keep their registry order and append after.
wp.os.applyWindowControls( 'edit-post', {
hide: [ 'core/focus-tab' ],
} );Other windows retain the full set. Pass null to clear the override.
A control declared inline never enters the global registry — it lives only on this window:
wp.os.applyWindowControls( 'edit-post', {
custom: [
{
id: 'my-plugin/star',
label: 'Star this draft',
icon: 'dashicons-star-filled',
placement: 'controls', // alongside close/min/max
order: 5, // before core/minimize (order 10)
onClick: ( ev ) => {
console.log( 'starred', ev );
},
},
],
} );placement: 'controls' puts the button inside the cluster. 'left' and 'right' are accepted but currently route through the legacy title-bar-button registry — see Connect to a window for the established pattern.
When the same control should appear in many windows, register it via wp.os.registerWindowControl() with a match predicate:
plugin.php
add_action( 'admin_enqueue_scripts', function () {
wp_register_script(
'my-plugin-controls',
plugins_url( 'controls.js', __FILE__ ),
array( 'openstation' ),
'1.0.0', true
);
wp_enqueue_script( 'my-plugin-controls' );
} );
openstation_register_window_control_script( 'my-plugin-controls' );controls.js
wp.os.whenReady( () => {
wp.os.registerWindowControl( {
id: 'my-plugin/info',
label: 'Info',
icon: 'dashicons-info',
placement: 'controls',
order: 5,
match: ( win ) => win.config.url?.includes( 'post.php' ) ?? false,
owner: 'my-plugin-controls', // for live unregister on deactivation
onClick: ( win ) => {
console.log( 'info clicked on', win.id );
},
} );
} );The owner field is the WP script handle. Deactivation drops every control with this owner without F5.
wp.os.unregisterWindowControl( 'core/focus-tab' );Re-register at any time to bring it back; the registry is a Map and entries replace by id.
wp.os.applyWindowControls( 'my-plugin/dashboard', {
placement: 'left',
} );Sets the os-window__controls--left class on the cluster, which moves it to the start of the title bar. A theme that draws the title bar differently can restyle that class.
When you don't want to register or unregister, use the os.window.chrome.controls filter to mutate the list at paint time:
wp.hooks.addFilter(
'os.window.chrome.controls',
'my-plugin/never-close-the-shop',
( controls, ctx ) => {
if ( ctx.placement !== 'controls' ) return controls;
// Hide close on the woocommerce shop window.
if ( ctx.config.url?.includes( 'admin.php?page=wc-admin' ) ) {
return controls.filter( ( c ) => c.id !== 'core/close' );
}
return controls;
}
);| Hook | Type | Signature | Purpose |
|---|---|---|---|
openstation_window_control_script_registered |
action | ( string $handle ) |
Fires after openstation_register_window_control_script() succeeds. |
openstation_window_control_registered |
action | ( string $id, array $entry ) |
Fires after openstation_register_window_control() stores metadata. |
| Hook | Type | Signature | Purpose |
|---|---|---|---|
os.window.chrome.controls |
filter | ( controls, { windowId, config, placement } ) => controls |
Mutate the resolved per-placement control list. Stable. |
os.window.chrome.applied |
action | ( { windowId, layer } ) |
Fires after a paint completes. layer is 'controls' for this layer. Stable. |
| Function | Purpose |
|---|---|
wp.os.registerWindowControl( def ) |
Register a global control. Throws on validation failure. |
wp.os.unregisterWindowControl( id ) |
Drop by id. No-op if not registered. |
wp.os.listWindowControls() |
Snapshot for tooling / inspectors. |
wp.os.applyWindowControls( windowId, override ) |
Per-window mutation at runtime. Pass null to clear. |
WindowConfig.appearance.controls |
Per-window declaration at registration time. |
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
- The App Framework — a window in one PHP file
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Data model — where OpenStation keeps its data
- 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
- Window-scoped MIO
- Mio
- Mobile — the phone layer
- Multisite
- Native Windows & Framework Interop
- OpenStation Network
- Plugin compatibility layer
- Pulse Monitor
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
- Workspaces
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration — Code Blue becomes an App Framework app
- Migration — AI comment scoring leaves core
- Migration: window, wallpaper and widget bundles load on demand
- Migration — Posts, Pages, Users, User Edit, Plugins and Comments become App Framework apps
- Migration — the navigation model
- Migration — OpenStation Preferences becomes an App Framework app
- Performance settings move to Extended options
- Presence storage migration
- Migration — the Recycle Bin becomes an App Framework app
- Migration — the shell boots from its own screen
- Migration — Station Home becomes an App Framework app
- Migration: a native window's tabs move to the window chrome
- Migration — WP Explorer becomes the my-wordpress app
- Migration — WordPress package globals are no longer ambient
More
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- App layout recipes
- 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
- Edit a record with one form values map
- 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
- Repairable form edits with MIO
- Register a window companion
- Pin your app to the phone tab bar, and react to the mode
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- WP Explorer — add a column to the list view
- 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
- Ship a window as an .os.php app
- 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
- Revisions in their own window — extend or redirect "View revisions"
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Observe stored-file cleanup failures
- 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
- Place something where the user can reach it — wp.os.workArea
- Ship a workspace template