Five minutes, a new dock icon, and a window that opens a custom URL.
Create a plugin alongside desktop-mode (anywhere under wp-content/plugins/):
<?php
/**
* Plugin Name: My Desktop Extension
*/
defined( 'ABSPATH' ) || exit;Activate it in WP Admin → Plugins.
The plugin exposes a single helper your code should use:
if ( function_exists( 'desktop_mode_is_enabled' ) && desktop_mode_is_enabled() ) {
// The current user has desktop mode on. Adapt behavior if needed.
}desktop_mode_is_enabled() returns true only when the active user has the desktop_mode_mode user meta set to '1'. If the plugin is inactive, the function does not exist — always guard with function_exists().
The dock is built from the admin $menu global by default. To surface a purely virtual entry (one that isn't in the admin menu), filter desktop_mode_dock_items:
add_filter( 'desktop_mode_dock_items', function ( $items ) {
$items[] = array(
'id' => 'my-extension-panel',
'title' => 'My Panel',
'icon' => 'dashicons-superhero',
'url' => admin_url( 'admin.php?page=my-extension' ),
'badge' => 0,
'submenu' => array(),
);
return $items;
} );Reload the shell; the new icon appears at the end of the dock. Click it and a window opens with admin.php?page=my-extension inside it.
Every HTTP call from a Desktop Mode plugin should route through the framework helper instead of native fetch(). Doing so lights up the active window's title-bar modem activity dot for free, attributes the request to the activity bus (so the dev panel + plugin observers see it), and flashes red on failure with the error message exposed as the dot's tooltip.
const res = await wp.desktop.fetch( '/wp-json/myplugin/v1/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify( payload ),
} );Same return type and resolution semantics as native fetch(). The third options arg attributes the request:
wp.desktop.fetch( url, init, {
windowId: 'my-plugin/inbox', // attribute to a specific window
silent: true, // background poll — don't pulse the dot
} );For modules compiled into a separate Vite target (a feature bundle, an external plugin), import trackedFetch from tracked-fetch:
import { trackedFetch } from '<…>/tracked-fetch';
await trackedFetch( '/wp-json/myplugin/v1/save', init, {
source: 'my-plugin/save',
} );trackedFetch finds wp.desktop.fetch at runtime and falls back to native fetch only during the boot window before the shell exists.
Lint enforces this. Raw
fetch()andwindow.fetch()calls fail lint. The handful of legitimate exceptions (the wrapper itself, the PWA service worker, genuinely-silent background pollers) are documented inline witheslint-disable-next-linecomments.
See javascript-reference.md for the full signature.
The shell dispatches CustomEvents on document when windows open, close, focus, or change state:
document.addEventListener( 'desktop-mode-window-opened', function ( e ) {
console.log( 'Opened', e.detail.windowId, e.detail.title );
} );Enqueue this file only in desktop mode:
add_action( 'desktop_mode_mode_init', function () {
wp_enqueue_script(
'my-extension-shell',
plugin_dir_url( __FILE__ ) . 'shell.js',
array(),
'1.0.0',
true
);
} );desktop_mode_mode_init fires inside the parent shell render — perfect for enqueueing shell-level code.
Block desktop mode for a specific user class:
add_filter( 'desktop_mode_mode_enabled', function ( $enabled, $user_id ) {
// Contributors stay in classic admin.
if ( user_can( $user_id, 'contributor' ) ) {
return false;
}
return $enabled;
}, 10, 2 );- Hooks Reference — the full filter + action list.
- JavaScript Reference — the event and
postMessageAPIs. - Examples — copy-paste recipes.