Requires Hide Web Elements Pro 25.1+ (this document describes 26.4). 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.
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, error, latest }], where latest is the newer version when an update is available), has(id) (true when that plugin is running), isReady(), features, apiVersion and hostVersion. Use register as shown above; 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. If a required plugin stops, this one stops too. |
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 referenceUI
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 ⚙ Manage 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. 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.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). Uninstall removes both.
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 }) → Promise { ok, status, text, json() } Cross-origin requests through the host's GM_xmlhttpRequest. The user can switch a plugin's network access off in ⚙ Manage (the promise then rejects), and the Manage 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, site-block or uninstall)
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 ⚙ Manage.
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. Combine with requires to depend on it.ctx.plugins() → [{ id, name, version, status }] for the other installed plugins.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:
Wait for reload (default) or Load right away.Each plugin has:
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.ctx.fetch, and see which hosts were contacted.ctx.storage. Tap twice to confirm..json file. Reload the page after importing.Below the list, Installed · runs on other sites shows plugins that are installed but limited to other sites (see below). Uninstalled plugins sit at the bottom with 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 or Load right away.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. If a plugin has network access switched off, its update check is skipped.
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 Run on setting only narrows it further and can never widen it.setup runs again.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 as of 25.3; 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 taken from the 25.3 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.