Requires Hide Web Elements Pro 25.1+ (this document describes 26.6). A plugin is a normal userscript that registers itself with the main script. It runs alongside it, and users manage plugins in the ⿻ Plugins tab.
The host now reports plugin API 2 (ctx.host.api, HWEPro.apiVersion). A plugin's api field is the minimum API it needs, so plugins that declare api: 1 keep working unchanged. Everything added after the first release is optional. To check for a newer feature, use ctx.host.features (e.g. ctx.host.features.includes('update-check')). Current features: settings, bus, expose, commands, timers, listen, observe, confirm, navigate, requires, export, network-control, update-check, performance, preset-sites, on-demand. The last three arrived after the first release of API 2, so a plugin that uses them should check ctx.host.features first (or simply tolerate them being ignored by an older host).
HWE-Plugin-Template.user.js).Plugin header: use @grant none so the plugin and the main script share the page's window. Install order doesn't matter.
const plugin = { id: 'my-plugin', name: 'My Plugin', version: '1.0.0', api: 1, setup(ctx) { /* ... */ } };
if (window.HWEPro) window.HWEPro.register(plugin);
else (window.__HWEPluginQueue = window.__HWEPluginQueue || []).push(plugin);
Only the top frame has the panel and dock, so start with if (window.top !== window) return;.
window.HWEPro objectThe host exposes a small object for plugins and other scripts: register(def), unregister(id), list() ([{ id, name, version, author, status, perf, frozen, error, latest }], where latest is the newer version when an update is available, perf is 'priority', 'normal' or 'freeze', and status is one of pending, off, waiting, running, sleeping or error), has(id) (true only when that plugin is running; a sleeping plugin is not running), isReady(), features, apiVersion and hostVersion. Use register as shown above; it returns { ok, id, status, error }. The rest is read-only information.
| Field | Notes |
|---|---|
id |
Required. 2–40 chars of a-z 0-9 - _. Registering the same id again replaces the old copy. |
setup(ctx) |
Required. Runs when the plugin starts. May return a cleanup function, or a Promise of one. |
name, version, author, description, icon |
Shown in the Plugins tab. |
homepage |
Optional http(s) link shown in the Plugins tab. A Greasyfork script page here is also used to find the plugin's update address. |
updateUrl, installUrl |
Optional http(s) links for update checking and re-installing. updateUrl may be the .meta.js or the .user.js address (updateURL, downloadUrl and downloadURL spellings also work). Greasyfork links are best, because the host can then also recognise the plugin in Get more (see Plugins that only run on some sites). |
settings |
Optional list of settings the host draws for you (see Settings). |
requires |
Optional list of plugin ids that must be running first, e.g. ['other-plugin']. Until then the plugin shows Waiting and starts automatically when they are ready. A required plugin that is Sleeping (see On-demand start) is woken for you. If a required plugin stops, this one stops too. |
performance (or perf) |
Optional default performance mode: 'priority', 'normal' (default) or 'freeze', or { mode, lock }. With lock: true the user cannot change it. See Performance modes. |
start, stub |
Optional. start: 'demand' lets the plugin sleep until it is needed; stub describes the light placeholder shown meanwhile. See On-demand start. |
sites, matches, excludeSites, excludes, access |
Optional. Decide where the plugin runs, so users don't have to. See Deciding where a plugin runs. |
api |
Plugin API version you need. If it is higher than the host's, the plugin is refused with a message. |
Renaming. The id is the key for a plugin's saved settings, storage, on/off state and site list. Change name, description and the userscript @name freely, but keep the id if users should keep their data. Changing @name makes most userscript managers treat the file as a new script, so tell users to remove the old one.
If setup throws, only that plugin stops. The error is shown in the Plugins tab and as a toast.
ctx referencectx.id is your plugin's id.
UI
ctx.addTab({ id, icon, title, label, html, render(root), onShow(root) }) → { root, show(), setLabel(text), remove() }
Adds a sidebar tab before Keys/About. Use html (a string) or render(root) (build DOM yourself). Plugin UI inherits the active theme, so use the built-in classes: hx-card, hx-menu-title, hx-menu-sub, hx-toggle-row, hx-check, hx-th-input, and buttons as hx-btn plus btn-gray, btn-blue, btn-green or btn-red. Custom panel styling should use the theme variables (see Following the HWE Pro theme). render is called again if the host rebuilds its UI.ctx.addDockButton({ id, icon, label, title, active, onClick }) → { el, setActive(bool), setIcon(x), setLabel(x), setTitle(x), remove() }
Adds a button to the main dock. Icon max 3 characters, label max 8. It gets the same theme styling as built-in buttons and appears in Theme → dock buttons, so users can hide, reorder or relabel it. The dock menu scrolls when it gets long.ctx.addDock({ id, side: 'left'|'right', top: '45%', icon, title, open, buttons: [{ icon, label, title, onClick, active }] }) → { el, addButton(def), setOpen(bool), remove() }
Adds a separate floating dock with its own toggle button.ctx.addStyle(css) → { remove() } CSS for the panel/dock UI (inside the shadow root).ctx.addPageStyle(css) → { remove() } CSS for the page itself.ctx.toast(message, opts) Shows the host toast.ctx.confirm(message, { ok, cancel, danger }) → Promise of true/false A themed yes/no dialog. Call it after the UI is ready (ctx.onReady); before that it falls back to the browser's confirm. Resolves false if the plugin is stopped while it is open.ctx.openTab(id) / ctx.closePanel() openTab takes your tab id, or a built-in id such as 'tab-rules'.Settings
Declare settings once and the host draws them in your plugin's Details & settings panel, saves them, and gives you a Reset settings button for free.
settings: [
{ key: 'enabled', type: 'toggle', label: 'Enable thing', default: true },
{ key: 'size', type: 'number', label: 'Size', min: 1, max: 10, default: 3 },
{ key: 'mode', type: 'select', label: 'Mode', options: ['fast', { value: 'slow', label: 'Slow & careful' }], default: 'fast' },
{ key: 'name', type: 'text', label: 'Name', placeholder: 'Anything' },
]
Types: toggle, text, textarea, number (min/max/step), select (options), color (#rrggbb). Each can have desc for a small hint, and hidden: true keeps a setting readable and writable through ctx.settings without drawing it in the panel (handy for internal state that should still be exported and reset with the rest). Keys are limited to A-Z a-z 0-9 _ . - and 40 characters. Define them in the plugin object, or at runtime with ctx.settings.define(list).
ctx.settings.get(key) / .set(key, value) / .all() / .reset(key?) Values are coerced to the type and clamped to min/max. Keys you never declared still read and write, just without coercion.ctx.settings.onChange((key, value) => {}) → unsubscribe. Fires when the user edits a setting or you call set.Settings survive Clear data (that only wipes ctx.storage).
Features
ctx.hide(selector, scope) → { ok, msg } Saves a normal hide rule. scope is 'site' (default), 'link' or 'global'. It then shows up in the Rules tab like any other rule.ctx.storage.get(key, default) / .set(key, value) / .remove(key) JSON values, kept separate per plugin.ctx.fetch(url, { method, headers, body, timeout, responseType }) → Promise { ok, status, url, headers, text, response, json() } Cross-origin requests through the host's GM_xmlhttpRequest (url is the final URL after redirects, headers the raw response headers, response the parsed body when you set responseType). The user can switch a plugin's network access off in Details & settings › Advanced (the promise then rejects), and that panel lists the hosts it contacted.ctx.addCommand({ id, title, icon, desc, hotkey, run }) → { run(), remove(), hotkey } Adds a named action. It is listed in the Plugins tab with a Run button. hotkey is optional, like 'Ctrl+Shift+Y' or 'Alt+Shift+J'. It is ignored while typing in a text field unless it uses Ctrl/Alt/Meta. Alt+Shift+<letter, digit or /> is used by the host's own shortcuts, so those are refused (hotkey is false in the returned handle).ctx.on(event, fn) → unsubscribe function. Events:
'ready': UI built, and again if it is rebuilt.'tab': { tab }, fires when the user switches tabs.'navigate': { url, from }, fires when the page URL changes without a reload (single-page apps). It is only watched while a plugin listens.ctx.onReady(fn) Runs now if the UI is ready, otherwise when it is.ctx.log(...), ctx.host ({ version, api, desktop, top, features }), ctx.getRoot() (the UI shadow root, for advanced use).Helpers that clean up after themselves (stopped automatically on disable, pause or site-block). They also follow the plugin's performance mode (see Performance modes): in the default mode repeating timers skip ticks while the tab is hidden and observer callbacks are batched.
ctx.setTimeout(fn, ms) / ctx.setInterval(fn, ms) / ctx.clearTimeout(id) / ctx.clearInterval(id)ctx.listen(target, type, fn, options) → remove function. Adds an event listener. options goes straight to addEventListener, so true (capture) and { passive: true } both work. Capturing on window for 'scroll' also catches scrolling inside inner containers.ctx.observe(target, callback, options) → { disconnect() }. A MutationObserver (default { childList: true, subtree: true }).ctx.onCleanup(fn) Extra cleanup, run when the plugin stops (in addition to the function setup may return).Errors thrown inside these callbacks are caught, stop nothing else, and are listed under Recent errors in Details & settings › Advanced.
Performance
ctx.performance.mode() → 'priority', 'normal' or 'freeze'. .isPriority() and .isFrozen() are shortcuts.ctx.performance.onChange((next, prev) => {}) → unsubscribe. Fires when the user (or Freeze all / Reset all) changes your mode.ctx.performance.defer(fn, timeout = 500) → cancel function. Runs fn when the browser is idle (or after timeout ms at the latest). Priority plugins run it on the next microtask. While frozen it is held until the plugin thaws. Use it for non-urgent work such as indexing or cache warm-up.Working with other plugins
ctx.bus.emit(topic, data) / ctx.bus.on(topic, fn) → unsubscribe. Messages go to every other running plugin listening to that topic. data is passed by reference.ctx.expose(api) Share an object other plugins can use.ctx.use(pluginId) → the exposed object of a running plugin, or null. If that plugin is Sleeping, calling ctx.use wakes it, but its setup may not have finished yet, so the first call can still return null; prefer requires for a hard dependency.ctx.plugins() → [{ id, name, version, status }] for the other installed plugins (status as in HWEPro.list()).The host tells the user about plugin changes with toasts:
version changes.setup fails or the plugin needs a newer host.The tab has three views: Installed, Get more and Settings. At the top, ⏸ Pause all (safe mode) stops every plugin at once, which is handy for troubleshooting. It becomes ▶ Resume while paused. The subtitle counts installed, running and sleeping plugins.
A ⟳ Check updates button checks every plugin that has an update address. A banner offers Load now when new plugins are waiting for a reload.
Each plugin row shows its icon, name, version, a ⚡ Priority tag if it has that mode, author, description, the plugins it needs, and a status chip: Running, Frozen, Sleeping, Waiting, Pending reload, Off, Paused, Not on this site or Error. It also has:
stub.stub the Start choice (Automatic / On demand). It also shows how long the plugin has kept the page busy.All sites, Only specific (a list of sites) or All except (a list of sites to skip). Sites match with their subdomains (example.com covers m.example.com), and a + site button adds the page you are on. Changes apply immediately, without a reload. This choice is hidden when the plugin decides for itself (see Deciding where a plugin runs).ctx.storage; tap twice to confirm) and Export data / Import data (save or restore a plugin's storage and settings as a .json file; reload the page after importing).There is no Uninstall button. To remove a plugin, delete its script in your userscript manager.
Below the list, Installed · runs on other sites shows plugins that are installed but limited to other sites (see below). Removed shows plugins uninstalled in an older version; they stay blocked and have a Restore button.
Lists the plugins and userscripts published by the author on Greasyfork, with install counts, ratings and versions. You can filter by All / Plugins / Userscripts, by Any / Installed / Not installed, and search by name. Installed plugins show a DETECTED tag and an Installed ✓ button. Install opens the script in the user's userscript manager, which asks for confirmation.
Wait for reload (default) or Load right away.Plugin default, Automatic or On demand, for plugins that support on-demand start.Every plugin runs in one of three modes. The user picks it per plugin in Details & settings › Performance; you can set the default (and optionally lock it) with the performance field.
| Mode | Behaviour |
|---|---|
| ⚡ Priority | Loads first and hears events first. Its timers keep running in hidden tabs, its observer callbacks are not batched, and it always starts automatically (it never sleeps). |
| ◎ Normal (default) | Repeating timers skip their ticks while the tab is hidden, and observer callbacks are delivered once per frame instead of on every small change (both can be switched off in Settings › Performance). |
| ❄ Freeze | The plugin keeps its tabs and buttons, but its intervals, ctx.listen listeners, ctx.observe observers, bus and page events and hotkeys stop. setTimeout callbacks that come due are held (up to 200) and run when it thaws. Listeners and observers are re-attached then. |
{ id: 'my-plugin', /* ... */ performance: 'normal' } // default, user can change it
{ id: 'my-plugin', /* ... */ performance: { mode: 'priority' } } // starts as Priority, user can change it
{ id: 'my-plugin', /* ... */ performance: { mode: 'normal', lock: true } } // user cannot change it
'high' is accepted for 'priority' and 'frozen' for 'freeze'. A frozen plugin is never unloaded, so don't rely on setup running again when it thaws: react in ctx.performance.onChange instead, and do heavy optional work through ctx.performance.defer. Use ctx.setInterval, ctx.listen and ctx.observe (rather than the raw browser versions) so that all of this applies to your plugin.
A plugin that is only used now and then can sleep: its setup doesn't run, and the host shows only a light placeholder (a stub) until the plugin is needed. This saves work on every page load. Add start: 'demand' and a stub to opt in:
const plugin = {
id: 'my-plugin', name: 'My Plugin', version: '1.0.0', api: 2,
start: 'demand',
stub: {
button: { id: 'open', icon: '★', label: 'Mine', title: 'Open My Plugin' },
tab: { id: 'main', icon: '★', title: 'My Plugin' },
commands: [{ id: 'run', title: 'Run it', icon: '▶', hotkey: 'Ctrl+Shift+Y' }],
selector: 'video', // optional: wake on pages that contain this element
urlMatch: [/example\.com\/watch/], // optional: wake on matching URLs (RegExp, or string compiled as a case-insensitive regex)
},
setup(ctx) {
ctx.addDockButton({ id: 'open', icon: '★', label: 'Mine', onClick() { /* ... */ } });
ctx.addTab({ id: 'main', icon: '★', title: 'My Plugin', html: '...' });
ctx.addCommand({ id: 'run', title: 'Run it', hotkey: 'Ctrl+Shift+Y', run() { /* ... */ } });
},
};
button, tab, commands, selector or urlMatch; without one the plugin always starts normally.ctx.use(id) or listing it in requires, or a page watcher. For selector and urlMatch the host checks about every 1.2 seconds, and when you give both, the page must match both.setup runs as usual, then the action that woke it is replayed: your button's onClick, your tab's onShow, or the command's run. For that to work, use the same ids in the stub and in your addDockButton, addTab and addCommand calls.Plugin default, then your start, then automatic. A change applies the next time the plugin starts. Priority plugins never sleep.HWEPro.has(id) is false while asleep and ctx.plugins() reports status: 'sleeping'. A sleeping plugin has no bus listeners or exposed API until it wakes.Normally the user decides under Where it runs. If your plugin only makes sense on certain sites, you can declare that and the choice is hidden:
{ sites: ['example.com', 'example.org'] } // only these sites (and their subdomains)
{ excludeSites: ['mail.example.com'] } // everywhere except these
{ matches: ['*://*.example.com/*'] } // userscript-style patterns, reduced to hosts
{ access: { mode: 'only', sites: ['example.com'], lock: true } } // explicit form
access wins over sites, then matches, then excludeSites, then excludes. A pattern of * or <all_urls> in matches/excludes is ignored, and a mode with no usable sites falls back to all sites. www. and ports are dropped from site names. The host still can't run your plugin where its userscript @match doesn't, so keep @match consistent with what you declare here.
The host looks up each plugin's update address and compares its @version with the running version. It tries, in order: the plugin's updateUrl/downloadUrl, an @updateURL / @downloadURL line or Greasyfork link found in the plugin's own definition or script, and finally a match by name on the author's Greasyfork page. Checks run about every 6 hours (and again an hour after a failure), when a page becomes visible, or when the user presses ⟳ Check updates (or ⟳ Check for update on one plugin). If a plugin has network access switched off, its update check is skipped. If nothing is found automatically, the user can type an update address in Details & settings › Updates.
To make updates reliable, give your plugin a Greasyfork updateUrl (or homepage), and keep version in the plugin object equal to the @version in the userscript header.
A plugin whose userscript has a narrow @match (for example only google.com) only registers on those sites, so on any other page the host never sees it. To keep such plugins visible:
updateUrl or homepage should point to your Greasyfork page.@match as narrow as the plugin needs. The host's Where it runs setting (or a sites declaration) only narrows it further and can never widen it.setup runs again (or, for an on-demand plugin, the stub is shown and setup runs when it wakes).setup runs again at once, even for an on-demand plugin.ctx is tracked and removed for you. You only need cleanup for things you created yourself (for example your own timers or observers made without ctx).The main panel sits at z-index: 2147483645. Anything of yours that needs to appear over it must sit higher, or it shows behind the menu.
ctx.toast is drawn above the panel. If a theme ever pushes it back, add ctx.addStyle('#hider-toast{z-index:2147483647 !important}').ctx.confirm dialogs use the .hwep-modal class, which is lower than the panel. Lift it with ctx.addStyle('.hwep-modal{z-index:2147483646 !important}').popover="manual", append it to document.documentElement, then call host.showPopover() (wrap in try/catch). Browsers without popover support fall back to z-index: 2147483647 and appending the host last.ctx.addStyle for panel UI (inside the shadow root) and ctx.addPageStyle for the page. The z-index rules above only apply to the panel's shadow root.The panel and dock follow the theme the user picked in Theme (dark, light, midnight, accent color, tint, radius, text size). A plugin can follow it too. How depends on where the UI lives.
ctx.addStyle)This already inherits the theme. Use the built-in classes, and for anything custom use HWE Pro's CSS variables with a fallback, so the style still works if a variable is missing:
ctx.addStyle(`
.my-card{background:var(--hx-surface,rgba(8,14,25,.78));border:1px solid var(--hx-line,rgba(148,163,184,.14));
border-radius:var(--hx-radius,14px);color:var(--hx-text,#e7eef8)}
.my-card small{color:var(--hx-muted,#718096)}
.my-card .go{background:var(--hx-cyan-strong,#38bdf8)}
`);
Variables defined on the panel root (names checked against 26.6; they are not part of the versioned API, so always give a fallback):
| Variable | Meaning |
|---|---|
--hx-bg |
Opaque base background |
--hx-panel, --hx-surface, --hx-surface-2 |
Translucent panel and card backgrounds (rgba) |
--hx-line, --hx-line-bright |
Subtle and bright borders (rgba) |
--hx-text, --hx-muted |
Main and secondary text |
--hx-cyan, --hx-cyan-strong |
Accent colors. They shift with the user's accent color |
--hx-blue, --hx-violet, --hx-green, --hx-red, --hx-gold |
Status and highlight colors |
--hx-radius |
Corner radius |
Light mode, accent color, tint and border settings are applied to these values by the host, so reading them gives the themed result.
The panel lives in a closed shadow root. Its variables do not reach your page overlays, and document.querySelector or getComputedStyle(document.documentElement) will not find them. To follow the theme:
el of a dock button from ctx.addDockButton, or the root passed to your tab's render.getComputedStyle.ctx.storage. The panel may not be mounted on every page load, and the cache gives you the last known theme.ctx.setInterval and compare with the last values.const NAMES = { bg: '--hx-bg', fg: '--hx-text', mu: '--hx-muted', bd: '--hx-line-bright', ac: '--hx-cyan-strong', rad: '--hx-radius' };
let last = '';
function readTheme(probe) {
if (!probe || !probe.isConnected) return null;
const cs = getComputedStyle(probe);
const t = {};
for (const k in NAMES) t[k] = cs.getPropertyValue(NAMES[k]).trim();
return t.bg && t.fg ? t : null;
}
function applyTheme(host, t) {
host.style.setProperty('--bg', t.bg);
host.style.setProperty('--fg', t.fg);
host.style.setProperty('--muted', t.mu);
host.style.setProperty('--line', t.bd);
host.style.setProperty('--accent', t.ac);
host.style.setProperty('--radius', t.rad || '14px');
}
setup(ctx) {
const btn = ctx.addDockButton({ id: 'open', icon: '★', label: 'Mine', onClick() {} });
const host = document.createElement('div'); // your overlay host
// ...attach a shadow root, add your overlay, append host to the page...
const sync = () => {
let t = readTheme(btn.el);
if (t) { const sig = JSON.stringify(t); if (sig === last) return; last = sig; ctx.storage.set('theme', t); }
else t = ctx.storage.get('theme', null);
if (t) applyTheme(host, t);
};
sync();
ctx.setInterval(sync, 1200);
}
In your overlay CSS, use the copied variables with defaults, for example background: var(--bg, #0b1220); color: var(--fg, #e7eef8); border-radius: var(--radius, 14px).
rgba(...)). If you need numbers (for a contrast check, or to make a darker accent), resolve the value through a hidden probe element: set its color to the value, read getComputedStyle(probe).color, and parse the rgb() string.0.2126*r + 0.7152*g + 0.0722*b > 127 ? '#04121f' : '#fff'.--hx-bg is opaque. --hx-panel and --hx-surface are rgba. For a glass look, mix the opaque color: color-mix(in srgb, var(--bg) 55%, transparent) with backdrop-filter: blur(24px) saturate(1.8).select setting such as Colors: Follow HWE Pro / Dark / Light, so users can override.ctx. The UI shadow root is closed to other scripts. Elements the host gives you (addDockButton(...).el, the tab root) are inside it, and you can read their computed styles.--hx-*) were last checked against the 26.6 source. They are stable in practice but are not part of the versioned API.hider_plugins_seen. Treat it as internal.@grant none can't see window.HWEPro on Firefox, tell me and I'll adjust the handshake.