-
Notifications
You must be signed in to change notification settings - Fork 41
icons
Stable.
OpenStation draws from a set of exactly thirty icons. Nineteen are WordPress's
own, taken from @wordpress/icons.
Eleven are drawn for this plugin. There is one rule for which is which, and it
is worth learning before adding any UI:
Core owns the verbs, OpenStation owns the nouns.
If WordPress already has a concept, its icon comes from Core and looks the way
it does in every other WordPress screen the user has ever seen. We draw one only
when the thing exists because this is a desktop and wp-admin is not. Save,
search, trash, settings and download are Core's every time. A window you can
drag is not a WordPress concept, so window is ours.
The eleven that are ours:
window · windows · dock · spaces · copilot · snap · command ·
apps · widgets · user · lock
Core's nineteen, under the name the shell uses for each:
close · check · chevron-right · arrow-up-right · plus · search ·
pin · trash · download · settings · info · bell · more ·
maximize · edit · color · wallpaper · warning · minimize
Six of those are the same concept under a different word upstream: more is
more-horizontal, maximize is fullscreen, edit is pencil, wallpaper
is image, warning is caution, minimize is line-solid. The shell says
what it means rather than what upstream filed it under.
There is deliberately no palette. It reads as command palette, which is
command in this very set, so the colour droplet takes Core's own name.
import { osIcon, osIconSvg, osIconDataUri } from '<…>/ui/icons';
// A node, for `html` template slots.
html`<button aria-label="Dismiss">${ osIcon( 'close', { size: 16 } ) }</button>`
// Markup, for string-built UI and `innerHTML`.
el.innerHTML = osIconSvg( 'trash', { size: 20 } );
// A data URI, for the `icon:` field of the dock / window / desktop-icon APIs.
wp.os.registerDockItem( { icon: osIconDataUri( 'spaces' ) } );Options: size (CSS pixels, default 24, null to let CSS own the box),
className, title, and rotate (90 / 180 / 270). The set ships one
chevron, pointing right, the way Core does; a menu that opens downwards asks
for rotate: 90 rather than a second drawing.
An unknown name renders nothing rather than throwing. A missing glyph is a blemish; an error inside a render pass takes the surface down with it.
Third-party plugins reach the same thirty through wp.os.iconSet, so a window
you register can wear the icons the shell wears instead of your own:
el.innerHTML = wp.os.iconSet.svg( 'trash', { size: 20 } );
button.append( wp.os.iconSet.node( 'close', { size: 16 } ) );
wp.os.registerDockItem( { icon: wp.os.iconSet.dataUri( 'spaces' ) } );
wp.os.iconSet.names // all thirty
wp.os.iconSet.ours // the eleven that are OpenStation's
wp.os.iconSet.has( 'window' ) // trueNot to be confused with wp.os.icons, which is the wallpaper icon rail's badge
and art API. The two are unrelated; the name here is singular-plus-Set for
exactly that reason.
The object is frozen, including its two lists. Every plugin on the page reaches the same one, so a reassignment would change what everyone else draws.
TypeScript plugins can import instead of reaching through the global:
import { osIconSvg, type OsIconName } from 'openstation';Icons are aria-hidden by default, because the common case is a glyph inside a
button that already carries its own label, and announcing both reads the control
twice. Pass title only when the icon is the sole carrier of meaning; it
becomes role="img" with an accessible name.
The default is 24, Core's native size and what WordPress renders these at. Core's glyphs carry 1.5-unit strokes on a 24 grid, so at 10px those are 0.6px wide and the shape goes faint. Below about 16px an icon stops being an icon and becomes part of the drawing it sits in: that is why the spinner's arc, the save-status check inside its 8px dot, and the Mio mark are drawn where they are used rather than taken from here.
Three groups stay hand-drawn on purpose, and each says so at the code:
-
Window chrome (
os-window-button,os-tab-chip). The set coversclosebut has nodetach,fullscreen-exitorreload, and neither does Core. Converting onlyclosewould put one filled glyph beside a monoline one inside a cluster two buttons wide, which is the exact inconsistency the set exists to remove. Chrome stays whole at its own 12-grid, 1.25-stroke weight. -
Dock tile art (
dock-shell-tiles.ts,gear-icon.ts,shortcuts.ts). A 64-grid family with heavier strokes, shipped asdata:URIs because the dock API takes anicon:string. Two of them have no member in the set at all: the gear is deliberately not Core'ssettings, because the System tile beside it already means settings. -
Marks and motion: the WordPress logo, the Mio face, spinners, stars, the
eye in
os-text-field. Not vocabulary.
Adding a thirty-first icon is a design decision rather than a drive-by
addition; tests/vitest/ui-icons.test.ts pins the counts so it cannot happen by
accident.
src/ui/icons/set.ts is generated from the brand repository and must not be
hand-edited. src/ui/icons/index.ts beside it is hand-written and is what call
sites import. Core's paths are copied verbatim, so a fix upstream is
re-exported, never re-drawn.
The same eleven appear twice more in the tree, for different consumers:
| Where | What | Why |
|---|---|---|
src/ui/icons/set.ts |
monoline, as drawn | what the shell renders |
assets/icons/*.svg |
outlines of the strokes | what WordPress's icon registry accepts |
| The brand repository | the sources | what everything else is generated from |
The outlines exist because WordPress sanitises registered icon markup through
wp_kses and keeps no stroke attribute, so a monoline icon registered as
drawn loses its stroke and renders as a blob. That constraint applies to the
registry only; nothing in our own shadow roots passes through wp_kses, which
is why the shell draws the real strokes. See
assets/icons/README.md and
includes/wp-icon-registry.php.
On WordPress 7.1 and newer the eleven are also reachable from PHP as
wp_get_icon( 'openstation/window' ). Every call is feature-detected, so the
plugin's 6.0 floor is unaffected.
24 x 24 grid, 17.5 live area, 1.5 stroke, round caps and joins, corner radius 2
(Core's value), currentColor and never a hex. currentColor is not a
preference: it is what routes art down the mask path in the dock and title-bar
painters, and a fixed fill survives neither. Start from an existing icon rather
than a blank grid.
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