Repository navigation
components reference
Canonical mapping of every shipped web component: tag name → exported class → source file → one-line purpose. The runtime missing-component warner (src/ui/components/missing-import-warner.ts) points readers here.
Components are side-effect registered at import time, per bundle, into the page-global custom-element registry. The shell bundle (desktop[.min].js) registers a core subset and pre-loads shell-overlays[.min].js (the toast / confirm-dialog / context-menu / menu / select / window-chrome kit) right after first paint, so those tags upgrade anywhere once the shell is up. The remaining tags load on demand. Every other component registers only when a bundle that imports its module loads — emitting a <os-foo> tag that no loaded bundle has imported renders inert HTML, and the missing-component warner logs a console.error with the exact import line to add.
Two ways to load the remaining components.
-
Import the module —
import 'openstation'(the package entry re-exports the barrel, so any import registers every tag) or a single leaf module. Right for code built inside this repo, or beside it via thefile:dependency inuse-from-a-plugin.md. The class export is only needed for TypeScript types or programmatic instantiation. -
Load the kit at runtime —
await wp.os.loadComponents( [ 'os-switch' ] ). No build-time relationship with this repo required, which is what a plugin distributed as a zip has. Seewp.os.loadComponents()for the cost, andexamples/load-components.mdfor a working panel.
Neither registers a tag twice: defineComponent() no-ops on a tag the registry already has, and the runtime loader skips the fetch when the tags asked for are all present.
src/ui/components/index.ts re-exports every component class AND the OS_COMPONENT_TAGS constant — the array all the dev-time guards iterate. The constant itself is defined in src/ui/components/tags.ts (the single source of truth, kept side-effect-free); the index re-exports it. If this doc and the index disagree, the index wins. To add a new component:
- Create
src/ui/components/<name>/<name>.ts,<name>.styles.ts,<name>.test.ts. - Add the class export to
src/ui/components/index.ts. - Add the tag to
src/ui/components/tags.ts(the single source ofOS_COMPONENT_TAGS, re-exported byindex.ts). - Add a row to this table.
- Document via the
static help = { … }block on the class — surfaced in OpenStation Preferences → Components live.
OpenStation Preferences → Components (admin-only) renders this table live: every tag in OS_COMPONENT_TAGS, with its props, slots, events, parts, CSS custom properties, and a working example rendered from the static help.example template.
The tab side-effect-imports the whole component barrel so the list is the full kit rather than "whatever other bundles happen to have loaded" — the per-bundle registration model described above means an unimported component reaches no custom-element registry, and a tab that only enumerated registered tags silently under-reported itself.
The search box above the list filters on the flattened descriptor, not just the title: tag name, summary, status, static props names, and the name and description of every documented prop, slot, event, part, and CSS custom property. Terms are ANDed and order-independent, so field number and number clamp both reach <os-number-field>.
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-body> |
OsBody |
os-body/os-body.ts |
Page-body scroll container. |
<os-panel> |
OsPanel |
os-panel/os-panel.ts |
Padded vertical grouping container. |
<os-section> |
OsSection |
os-section/os-section.ts |
Titled section block within a panel. |
<os-row> |
OsRow |
os-row/os-row.ts |
Twelve-track grid; children declare column widths with col. |
<os-stack> |
OsStack |
os-stack/os-stack.ts |
Vertical flex stack with consistent gap. |
<os-cluster> |
OsCluster |
os-cluster/os-cluster.ts |
Wrapped flex row for chips / tags / actions. |
<os-app-frame> |
OsAppFrame |
os-app-frame/os-app-frame.ts |
Stable: persistent header, toolbar and footer around a scrolling or contained body. |
<os-split> |
OsSplit |
os-split/os-split.ts |
Stable: responsive panes with pointer/keyboard resizing and explicit narrow-pane selection. |
<os-grid> |
OsGrid |
os-grid/os-grid.ts |
Fixed or automatically fitting columns with child column/row spans. |
<os-card> |
OsCard |
os-card/os-card.ts |
Bordered surface for entity-card UIs. |
<os-display> |
OsDisplay |
os-display/os-display.ts |
Hero / display-typography container. |
<os-disclosure> |
OsDisclosure |
os-disclosure/os-disclosure.ts |
Titled section that folds away. Closed by default. Parts: summary, heading (compact typography), body. |
See app layout recipes for sizing, scrolling, spans, narrow layouts and the split event contract.
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-form> |
OsForm |
os-form/os-form.ts |
Form host with auto value-collection + validation. |
<os-field-row> |
OsFieldRow |
os-field-row/os-field-row.ts |
Label + control + hint + error. Wires the accessible pairing a light-DOM control can't get from a shadow-root <label for>: aria-describedby, aria-invalid, required, click-to-focus. |
<os-token-field> |
OsTokenField |
os-token-field/os-token-field.ts |
Text / textarea whose value contains tokens ({field:2}, {all_fields}), with a grouped catalogue that inserts at the caret and a live "reads as" preview built from each token's sample. |
<os-repeater> |
OsRepeater |
os-repeater/os-repeater.ts |
Add / remove / reorder a list of rows whose content you supply. Keyed by stable strings, so a remove or move never rebuilds the rows that didn't change. Reports intent; the consumer owns the data. |
<os-text-field> |
OsTextField |
os-text-field/os-text-field.ts |
Single-line text input. |
<os-textarea> |
OsTextarea |
os-textarea/os-textarea.ts |
Multi-line text input. auto-grow expands to max-rows, including the border, then scrolls vertically; Enter submission respects IME composition. |
<os-number-field> |
OsNumberField |
os-number-field/os-number-field.ts |
Numeric input with min/max/step. |
<os-color-field> |
OsColorField |
os-color-field/os-color-field.ts |
Color picker with swatches. |
<os-range-field> |
OsRangeField |
os-range-field/os-range-field.ts |
Slider with live numeric readout. |
<os-checkbox> |
OsCheckbox |
os-checkbox/os-checkbox.ts |
Standalone checkbox. |
<os-checkbox-label> |
OsCheckboxLabel |
os-checkbox-label/os-checkbox-label.ts |
Checkbox + inline label pair. |
<os-switch> |
OsSwitch |
os-switch/os-switch.ts |
On/off switch for settings that apply immediately. Tap, drag or keyboard. |
<os-select> / <os-option>
|
OsSelect, OsOption
|
os-select/os-select.ts |
Combobox trigger with a custom top-layer listbox popup. |
<os-multiselect> |
OsMultiselect |
os-multiselect/os-multiselect.ts |
Multi-select with chips. |
<os-segmented> / <os-segment>
|
OsSegmented, OsSegment
|
os-segmented/os-segmented.ts |
Segmented control (radio group as buttons). |
<os-tag-input> |
OsTagInput |
os-tag-input/os-tag-input.ts |
Free-text tag entry with autocomplete. |
<os-category-picker> |
OsCategoryPicker |
os-category-picker/os-category-picker.ts |
Category tree picker. |
<os-role-picker> |
OsRolePicker |
os-role-picker/os-role-picker.ts |
WP role select. |
<os-user-search> |
OsUserSearch |
os-user-search/os-user-search.ts |
Live user autocomplete (/desktop-mode/v1/files/users/search REST). |
Give each field a name and use getValues() / setValues(patch) for the whole
record. Checkboxes and switches return booleans; tag inputs retain their array
of { label, id? } objects. Scalar fields retain their existing string values;
convert numbers explicitly at the storage boundary. Structured values are assigned
through the component's value setter and are not serialized into attributes.
reset() restores the initial field values, including checked switches and tags.
Structured-cloneable data is copied at capture and on each reset, so editing a
tag object or array cannot overwrite those defaults. Opaque, non-cloneable custom
field values retain their existing identity semantics.
setBusy(true) makes the fields inert and blocks button, Enter and programmatic
submission until cleared. Set it before the first asynchronous operation and clear
it in finally. Existing per-field disabled settings are preserved.
os-form-input reports named text, checkbox/switch, select, range and color changes.
Tag add/remove events are intents: the app still updates tags.value explicitly;
setValues() does not emit user-input events.
See editing a mixed-field record for a complete load/save/reset pattern.
Date fields (date, datetime-local, month, week) keep the browser's native
picker. In browsers exposing the calendar indicator styling hook, its glyph reads
--os-ui-fg-muted, matching the field's other affordances across dark and light
themes. Forced-colors mode uses the system button text color.
The desktop shell is a real wp-admin document, so WordPress's own
forms.css is loaded and reaches every raw control you put in it:
input[ type="text" ], … , select, textarea {
background-color: #fff;
color: #1e1e1e;
border: 1px solid #949494;
}That selector weighs (0,1,1) — one type plus one attribute — which
outranks any single class of your own. A field you had already
tokenized still renders as a white core-chrome box, and on a dark
surface (a dialog, an overlay, a themed panel) the result is a bright
rectangle whose contents are painted by whichever rule did win. If
the control also pre-selects its text, the shell's ::selection —
tuned for dark surfaces — lands light ink on a pale wash over that
white, and the value is effectively unreadable.
The form components above live in shadow DOM, where forms.css cannot
follow, and they resolve the palette and the active desktop theme
instead. Use them. Raising specificity works too and is what
.os-window__body :is( input[ type ], … ) in window-chrome.css does
for admin markup we don't control — but for markup you are writing,
the component is the fix that stays fixed.
Slotting a component into a dark light-DOM surface, re-point the token
family the way <os-modal> does on its host, so the field resolves
dark-surface colours:
.my-plugin-dialog {
/* Foreground. */
--os-ui-fg: var( --os-ui-modal-text, #f0f0f1 );
--os-ui-fg-muted: var( --os-ui-modal-text-muted, #a7aaad );
--os-ui-border: var( --os-ui-modal-border, rgba( 255, 255, 255, 0.25 ) );
--os-ui-border-strong: var( --os-ui-modal-border-strong, rgba( 255, 255, 255, 0.35 ) );
/* Surface — the half that is easy to forget. */
--os-window-bg: var( --os-ui-modal-field-bg, #2c3338 );
--os-ui-surface: var( --os-ui-modal-surface, #2c3338 );
--os-ui-surface-elevated: var( --os-ui-modal-surface-elevated, #3c434a );
/* Washes, which are read against the surface they sit on. */
--os-ui-hover: var( --os-ui-modal-hover, rgba( 255, 255, 255, 0.08 ) );
--os-ui-button-bg-hover: var( --os-ui-modal-button-bg-hover, rgba( 255, 255, 255, 0.08 ) );
/* Cards. Their own tokens fall through to --os-ui-surface, so the
re-point above already reaches them — unless a desktop theme pins
one flat (Legacy: --os-ui-card-bg: #fff). Own the set. */
--os-ui-card-bg: var( --os-ui-modal-surface, #2c3338 );
--os-ui-card-fg: var( --os-ui-modal-text, #f0f0f1 );
--os-ui-card-border: var( --os-ui-modal-border, rgba( 255, 255, 255, 0.25 ) );
--os-ui-card-border-hover: var( --os-ui-modal-border-strong, rgba( 255, 255, 255, 0.35 ) );
}Read a palette-owned --os-ui-modal-* name first in every one of
them. Declaring the literal directly would make the name unreachable
from a desktop theme — the same trap described in
desktop-themes.md.
Take the whole list, not the colours you notice. Foreground and
surface are a pair, and re-pointing only the half you can see is a bug
that hides itself: while the palette outside your dialog is dark, every
token you left behind happens to agree, and the dialog looks right. Put
a light palette outside it — Legacy,
or any theme in the admin's own colours — and the halves come apart.
--os-ui-surface is the one that bit us: it stayed #fff, so an
<os-select> in the dialog painted a white trigger and the re-pointed
--os-ui-fg wrote near-white text onto it. --os-ui-hover was a black
wash over a dark row.
If your surface is dark, you own every token that names a surface or a wash on one — not just the text.
Shadow DOM has a second consequence, and the shell leans on it. A
keydown typed into a component's inner <input> reaches document
with its target retargeted to the host — OS-TEXT-FIELD, not
INPUT — so any third-party script guarding a bare-letter shortcut
with e.target.tagName === 'INPUT' fires while the user is typing.
The shell's text-entry guard (src/text-entry-guard.ts, documented
in javascript-reference.md)
stops a printable, unmodified keydown aimed at a text-entry element in
a shadow root at window's capture phase — before it reaches the
input.
For a component that means: characters arrive through input /
beforeinput; keydown carries everything else. Enter for
os-submit, Escape to close a popup, the arrows for a listbox,
Backspace on an empty tag field, Tab — all of those still reach the
input's listeners, because none of them is a single printable key
without a modifier. A type-ahead on a button or listbox trigger
(<os-select>) is untouched too; the guard only covers inputs,
textareas and contenteditable. What will not work is a text field
that watches keydown for the letter itself — e.key === ',' to
split a tag, say. Do that on input, where the character is already
in the value, or on beforeinput, where it can still be refused.
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-button> |
OsButton |
os-button/os-button.ts |
Primary / secondary / ghost button. |
<os-window-button> |
OsWindowButton |
os-window-button/os-window-button.ts |
Title-bar icon button (minimize / maximize / close / custom). |
An icon-only <os-button> names itself through aria-label on the
host. The focusable element is the <button> inside the shadow root
and the host has no role, so a name left on the host alone is inert;
the component forwards aria-label onto that inner button, keeps it
in sync when you relabel the host, and drops it from the inner button
when the host has none. aria-labelledby / aria-describedby are not
forwarded: an IDREF on the shadow <button> resolves inside that
shadow root only, so it could never reach an id in your markup.
<os-window-button disabled> forwards disabled state to its native button, preventing activation and keyboard focus. Optional aria-pressed="true|false|mixed" is forwarded to that same focusable button; active controls its visual pressed state.
<os-window-button> paints an aria-hidden glyph inside a shadow
<button>, so it has no accessible name of its own — always set
aria-label on the host. The component forwards it onto that inner
button (which is the element focus lands on) and keeps it in sync when
you relabel the host, e.g. Maximize ⇄ Restore.
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-menu> / <os-menu-item>
|
OsMenu, OsMenuItem
|
os-menu/os-menu.ts |
Dropdown menu surface. |
<os-action-menu> |
OsActionMenu |
os-action-menu/os-action-menu.ts |
Button-anchored dropdown with top-layer placement, arrow/Home/End navigation, Escape/outside dismissal and focus restoration. Accepts translated text and accessible label; wraps context-menu options and their os-context-menu-pick event. |
<os-context-menu> / <os-context-menu-option>
|
OsContextMenu, OsContextMenuOption
|
os-context-menu/os-context-menu.ts |
Right-click / long-press menu. |
<os-flyout> |
OsFlyout |
os-flyout/os-flyout.ts |
Anchored popover. Supports placement strategies. |
<os-coachmark> |
OsCoachmark |
os-coachmark/os-coachmark.ts |
Anchored callout with a step counter ("1 of 5"; pass a translated one through counter-label). Floats a neutral card beside the anchor element in the top layer, its tail aimed at it, no scrim; a thin neutral ring traces the anchor too unless highlight="none". An anchor that leaves the document or stops being rendered counts as no anchor until it is back; os-coachmark-primary / -secondary / -dismiss. The peek slot holds a small figure peeking over the card's top edge, which can read --os-coachmark-look-x / -y to look at the anchor. speaker-size turns the card into a speech balloon for a speaker standing beside it instead (across from the anchor, never between the two), reported through os-coachmark-speaker. Fades in and out, and glides between anchors on a step change. The shell tour is built from it, with Mío in the peek slot. |
<os-tooltip> |
OsTooltip, attachTooltip
|
os-tooltip/os-tooltip.ts |
Hover / keyboard-focus hint for a control whose glyph doesn't say what it does. Use attachTooltip( el, content ), not the tag. |
<os-modal> |
OsModal |
os-modal/os-modal.ts |
Full-overlay modal with focus trap. |
<os-confirm-dialog> |
OsConfirmDialog, osConfirm
|
os-confirm-dialog/os-confirm-dialog.ts |
Confirm prompt — use await osConfirm({...}) (never window.confirm). |
<os-toast> / <os-toast-container>
|
OsToast, OsToastContainer
|
os-toast/os-toast.ts |
Top-right (top inline-end) toast notifications. |
<os-notice> |
OsNotice |
os-notice/os-notice.ts |
Inline informational/warning notice. |
<os-tooltip> and attachTooltip(). attachTooltip( el, content, { delay } ) gives a control a tooltip and returns a function that removes it. content is a string, { heading, text }, or a function returning either (or null to skip), resolved on every show, so it can describe state that changes while the control is on screen. Every attached control shares ONE <os-tooltip> on document.body, fixed-positioned so a window's overflow or transform cannot clip it, and placed below the control, flipped above when there is no room, and kept inside the viewport. It shows after a hover delay (500ms by default) or at once on keyboard focus; it hides on leave, press, blur or Escape; it never shows for touch, or while the control reports aria-expanded="true". It is a visual aid only: the control still needs its own accessible name. Colours come from --os-tooltip-bg / --os-tooltip-fg, the same pair the dock's tooltip reads.
<os-toast> tone. A tone attribute (positive | warning | critical | neutral) paints a coloured edge and a leading icon from the palette's notice tokens (--os-ui-notice-success, -warning, -error, -neutral), so a failure toast and a failure <os-notice> read as a set. Without it the toast is the plain dark chip. showToast() sets it from its type option through the server's toast-type registry, so a caller names a type (error), never a tone.
<os-toast> hold contract. A toast reports when the user is attending to it — pointer over it, or focus anywhere inside it, including its action and close buttons in the shadow root. While that is true it carries a reflected held attribute and, on every transition, emits os-toast-hold with { held: boolean }. showToast() listens and pauses the auto-dismiss countdown for the duration; a released countdown resumes with the time it had left, floored at 1.2s so a nearly-expired toast doesn't vanish the instant the pointer leaves. held is set by the component and is not something to write by hand — a toast that should never expire on its own is persistent. Dismissing a toast that currently holds focus hands focus back to the last element outside the toast stack that had it, so clicking Undo never drops the user on <body>.
<os-confirm-dialog> "don't ask again". rememberLabel (attribute: remember-label) renders a checkbox above the buttons, and its state rides along on the os-confirm detail as remember; the osConfirm() wrapper surfaces it as an onRemember( remember ) callback so the Promise stays a plain Promise<boolean>. onRemember fires only on confirm — a question the user backed out of was never answered, so it cannot have meant "stop asking". Offer the checkbox only where you have somewhere to persist the answer and somewhere to turn it back on: an opt-out with no way back is a trap. The shell's own use is the close-all-windows shortcut, which persists to confirmCloseAllWindows in OpenStation Preferences → Windows.
<os-confirm-dialog> keyboard contract. Opening the dialog remembers what had focus and moves focus inside; Tab and Shift+Tab cycle between the dialog's own controls and cannot reach the page behind the scrim; closing — by any route, including osConfirm() unmounting the element — hands focus back to the control that opened it. Escape always cancels. Enter is the dialog's default action only while no control inside it owns the key: with a button focused, Enter activates that button, so Enter on Cancel cancels. A danger dialog has no default action — it opens on the safe control (Cancel, or the X that dismissable adds, or the container when hideCancel leaves neither) and never on its destructive button, and Enter from the container does nothing. Reaching the destructive action is always deliberate: Tab to it, or click it.
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-icon> |
OsIcon |
os-icon/os-icon.ts |
Icon by dashicon slug or SVG content. |
<os-avatar> |
OsAvatar |
os-avatar/os-avatar.ts |
User avatar with presence dot. |
<os-badge> |
OsBadge |
os-badge/os-badge.ts |
Number badge with tone color. |
<os-ribbon> |
OsRibbon |
os-ribbon/os-ribbon.ts |
Corner ribbon for tiles. |
<os-chip> |
OsChip |
os-chip/os-chip.ts |
Tag/category chip with tone. |
<os-key> |
OsKey |
os-key/os-key.ts |
Keyboard shortcut display. |
<os-code> |
OsCode |
os-code/os-code.ts |
Inline / block monospace code with copy; wrap folds long lines instead of scrolling them sideways. |
<os-spinner> |
OsSpinner |
os-spinner/os-spinner.ts |
Loading spinner with preset variants; preset="inline" is a bare arc for text-adjacent use. |
<os-progress-bar> |
OsProgressBar |
os-progress-bar/os-progress-bar.ts |
Determinate or indeterminate progress. |
<os-save-status> |
OsSaveStatus |
os-save-status/os-save-status.ts |
Save indicator (idle / saving / saved / failed). variant="ring" is the window title bar's status ring: outline for every phase but success, which fills. |
<os-relative-time> |
OsRelativeTime |
os-relative-time/os-relative-time.ts |
Auto-updating "2 min ago". |
<os-histogram> |
OsHistogram |
os-histogram/os-histogram.ts |
Stacked time histogram (inline SVG) with a toggle legend; series + columns JSON in, os-series-toggle out. Colours ride the status tokens. |
<os-facts> / <os-fact>
|
OsFacts, OsFact
|
os-facts/os-facts.ts |
Label/value list — a real <dl> whose rows are <os-fact label="…"> children carrying the value in their default slot (an <os-code>, an <os-relative-time>, a link, a badge). layout="between" spreads each pair across its own line; stacked puts the label above the value. An <os-code> value loses its snippet chrome and keeps the copy affordance; a theme can restore it through --os-ui-facts-code-{bg,border,padding,font-size}. |
<os-stat> |
OsStat |
os-stat/os-stat.ts |
One stat tile: big value, small uppercase label, optional caption; swatch adds a severity chip coloured by the app tone contract (data-tone on the host). |
<os-empty-state> |
OsEmptyState |
os-empty-state/os-empty-state.ts |
Empty-list / no-results placeholder. |
<os-rating-summary> |
OsRatingSummary |
os-rating-summary/os-rating-summary.ts |
Star average + per-star bucket bars. |
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-table> |
OsTable |
os-table/os-table.ts |
Sortable, filterable data table with sub-tables. A cell value shaped { slot, text? } renders a named slot a light-DOM child fills, so a server view can put a control in a row without a render function. stacked lays every row out as a card — the first column its title, the labelled ones captioned lines, a label-less one the actions row (column.stack overrides the role) — for a phone or any width the columns cannot fit; stack-on-phone.ts makes that decision from the shell's mode stamp for every list window. |
<os-log> |
OsLog |
os-log/os-log.ts |
Virtualized streaming log container. |
<os-tile> |
OsTile |
os-tile/os-tile.ts |
Desktop-style icon tile (used by the desktop file layer, folder windows, and WP Explorer). selectable switches it from listitem to option so it can carry aria-selected — the selection controller sets it. |
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-tabs> / <os-tab> / <os-tabpanel>
|
OsTabs, OsTab, OsTabPanel
|
os-tabs/os-tabs.ts |
Tab strip with associated panels, for a tab group inside content. A window's own top-level tabs belong in the window chrome instead — see Window.setTabs() in javascript-reference.md. |
<os-tab-chip> |
OsTabChip |
os-tab-chip/os-tab-chip.ts |
Single chip tab (e.g. window tabs). |
<os-steps> / <os-step>
|
OsSteps, OsStep
|
os-steps/os-steps.ts |
Numbered steps, stacked or as a horizontal trail. current marks where the reader is, interactive makes a step a jump target with a hover state. |
<os-crumb-chain> |
OsCrumbChain |
os-crumb-chain/os-crumb-chain.ts |
Breadcrumb trail with chevron separators. |
| Tag | Class | Source | Purpose |
|---|---|---|---|
<os-swatch> |
OsSwatch |
os-swatch/os-swatch.ts |
Single color swatch button. |
<os-swatch-grid> |
OsSwatchGrid |
os-swatch-grid/os-swatch-grid.ts |
Grid of color swatches with selection. |
The kit wears the OpenStation brand, and the brand ships five mesh gradients with one instruction attached: "meshes reserved for hero surfaces." src/ui/holo.ts is how a control gets to be one without every component reinventing what holographic means.
It is a moment, not a skin. Form controls (checkboxes, radios, switches, sliders) wear the flat accent when they are on, and so does the segmented thumb unless the palette says otherwise (the OpenStation palette makes it a mid-grey key on a Void track); selection and state across the kit resolve through --os-ui-accent, which follows the accent the user picked in OpenStation Preferences; the mesh appears only where a single surface speaks for the brand, such as <os-button variant="holo">. A panel where every surface is iridescent has no identity moments left to spend.
Three treatments, in ascending loudness:
| Fragment | What it does | Who gets it |
|---|---|---|
holoEdge |
An iridescent hairline, invisible at rest, lit on hover and focus. |
<os-button> (all variants but link/danger), and the selected state of <os-chip> and <os-card> (the selected <os-swatch> wears a flat accent ring instead). |
holoSheen |
A ~10%-alpha film of the mesh's hues over the existing surface, faded in under the pointer. |
<os-button>, <os-key>, unselected <os-segment>. |
holoFill |
The mesh itself, at full strength, with Void ink on top. | Checked <os-menu-item>, the fill of <os-progress-bar>, the <os-step> chip, and <os-button variant="holo">. Form controls — the on <os-switch>, checked <os-checkbox> / <os-checkbox-label>, the selected <os-segment>, the elapsed <os-range-field> track — wear the flat accent instead. |
…and five motions, which are what make the surfaces read as foil rather than as paint:
| Fragment | What it does | Who gets it |
|---|---|---|
holoGlint |
A specular band crosses the surface once on hover. The single most "holographic" thing in the kit. |
<os-button>, <os-key>, interactive <os-card>. |
holoRing |
A ring expands out of the control and fades on :active, so a press reads as received before its result paints. |
<os-button>, <os-key>. |
holoShimmer |
The mesh travelling, for waits of unknown length. | Indeterminate <os-progress-bar>. |
holoDrift |
The mesh slowly traversing the surface, running for as long as the state lasts. |
<os-button variant="holo" busy>. |
holoEnter |
Scale-and-fade arrival on the spring. |
<os-menu>, <os-context-menu> (<os-modal> and <os-confirm-dialog> arrive on their own dialog keyframes). |
Plus the motions that belong to one component and stayed there: the <os-segmented> thumb that slides between segments, the <os-tabs> underline that grows from the centre, the <os-switch> knob's spring-and-squash, the <os-checkbox> tick landing with an overshoot, <os-toast> arriving from above and leaving sideways, and the <os-avatar> presence ring — which pulses only on online, so "who is here" survives being read by someone who cannot separate the three dot colours.
The pseudo-element budget. An element has two, and this module wants four effects. holoSheen takes ::before and holoEdge takes ::after — that is the whole budget for a control wearing both, as <os-button> does. So holoGlint and holoRing are element-based: the component stamps a <span class="os-holo-glint"> / <span class="os-holo-ring"> and the fragment styles it. Both are driven from the parent's state via the child combinator (:active > .os-holo-ring), which is load-bearing: :active matches an activated element and every ancestor of it, so a descendant selector would fire every ring on the page.
Two shared fragments carry the states that are not decorative:
-
holoField— one hover, one focus ring, one transition duration and one placeholder colour for every text-like control (<os-text-field>,<os-textarea>,<os-number-field>,<os-select>, and any component that renders a bareinput/select/textareain its shadow root). Its selectors wrap their type exclusions in:where()so a component's ownaria-invalidring still outranks it — an invalid field focuses in red, not in Pulse. -
holoCheck— the checkbox and radio paint, shared by<os-checkbox>,<os-checkbox-label>and<os-table>'s selection column. It replacedaccent-colorso the checked paint resolves through--os-ui-accent— following the accent the user picked — instead of a browser-painted colour the tokens can't reach.
Declared in assets/css/variables.css, on body.os-active (never :root — the file also loads inside every iframe window). Every component reads them through a private --_holo-* alias, so a desktop theme can re-point any of them and the whole kit changes together.
| Token | Meaning |
|---|---|
--os-mesh-holo / -pulse / -auro / -star / -mio
|
The brand's five meshes, transcribed stop-for-stop from the SVGs into CSS gradient stacks. |
--os-ui-holo-fill |
What an "on" surface paints. Holomesh by default. |
--os-ui-holo-ink |
Glyphs and text on that fill. Void — every mesh in the brand is a light surface. |
--os-ui-holo-sheen |
The hover film. |
--os-ui-holo-edge / --os-ui-holo-edge-quiet
|
The iridescent hairline, lit and at rest. |
--os-ui-holo-glow / --os-ui-holo-glow-strong
|
The Pulse bloom around a lit surface. |
--os-ui-holo-track |
The unlit half — switch tracks, empty progress. |
--os-ui-tab-edge |
The selected row's leading edge in a vertical <os-tabs>. The flat accent, so the row says "this one" in the same colour as every control beside it. |
--os-ui-tab-wash / --os-ui-tab-bloom
|
That row's surface wash, and the bloom the edge throws back across it. Both ambient, so both resolve through --os-ui-accent-dim. |
--os-ui-tab-edge-width / --os-ui-tab-bloom-opacity / --os-ui-tab-wash-opacity
|
How much of the edge, the bloom and the accent wash show. 2px, 1 and 1 by default, the accent line with its glow and wash; 0px, 0 and 0 leave the row to its fill. |
--os-ui-segmented-selected-accent / --os-ui-segmented-selected-base
|
How much of the accent the selected <os-segmented> pill and its label take, as a percentage mixed towards -base (the pill) and --os-ui-fg (the label). The OpenStation palette answers 0% and a mid-grey that clears 3:1 against its Void --os-ui-segmented-bg track, so the control ignores the picker; unset is 100%, the accent pill. --os-ui-segmented-selected-bg / -fg still override both outright. |
--os-ui-segmented-edge / --os-ui-segmented-selected-shadow
|
Box shadows for the track's edge and under the selected key. The OpenStation palette answers a 10% Starlight hairline and a small drop shadow; unset is none for both. |
--os-ui-segmented-hover-bg / --os-ui-segmented-hover-sheen
|
What an unselected segment shows under the pointer: a shade, and the holographic film. The OpenStation palette answers a faint Starlight lift and none; unset is no shade and the kit's film. |
--os-ui-coachmark-ring / --os-ui-coachmark-action-bg / --os-ui-coachmark-action-fg
|
The <os-coachmark> ring, and its primary button's fill and label. Not the accent: the ring is the card's text colour at 45%, the button the card's text and surface swapped, because a coachmark usually points at UI that already uses the accent for its own state. |
--os-ui-coachmark-peek-reveal |
How much of a peek figure shows above the card's edge. 23px. |
--os-ui-tab-fill / --os-ui-tab-radius / --os-ui-tab-inset
|
The selected row's flat fill, corner radius and distance from the sidebar's edges. transparent, 0 and 0 by default, the full-bleed row; a fill with a radius and an inset makes it a pill. |
--os-ui-swatch-ring-width / --os-ui-swatch-lift / --os-ui-swatch-badge-bg
|
How a chosen <os-swatch> tile is marked: the width of its accent ring, a lift (stroke plus shadow) drawn with it, and the tick badge in its corner (transparent removes it). |
--os-ui-accent-dim |
Pulse one step back (same hue, S and L pulled down together). The single knob for how loud the station is — every ambient use of the accent resolves through it. |
--os-ui-focus-ring |
The target ring: buttons, switches, checkboxes, swatches. Built to survive landing on a bright mesh. |
--os-ui-focus-ring-field |
The field ring: quieter, tightens the input's own border. A form of twelve inputs should not look alarmed. |
--os-ui-motion-fast / --os-ui-holo-transition / --os-ui-motion-slow / --os-ui-motion-ambient
|
The duration scale: a state flip, the default tilt, something crossing a distance, an ambient loop. |
--os-ui-ease-spring / --os-ui-ease-out / --os-ui-ease-loop
|
The three curves. spring overshoots ~9% and is wrong for anything that changes size. |
--os-ui-accent is Pulse #f252fc and stays there — it is what the brand guidelines name, and brand-palette.test.ts pins it. Pulse is also not a contrast problem: it carries 6.2:1 against Obsidian.
What makes a panel read as loud is the ambient use — a bloom behind a focused control, an 18% wash under a selected row, a fill wider than a chip. Those all resolve through --os-ui-accent-dim, so:
body.os-active { --os-ui-accent-dim: #b02ab8; } /* quieter still */is the whole edit. The focus ring deliberately does not follow it — only the bloom behind the ring does. A focus indicator is the last place to trade legibility for calm.
Every fragment honours prefers-reduced-motion by stopping the tilt — never by removing the fill. A control that lost its mesh under reduced motion would lose its state, not just its animation.
import { css } from '../../core';
import { holoTokens, holoEdge, holoFill } from '../../holo';
export const styles = css`
${ holoTokens }
${ holoEdge }
${ holoFill }
button:focus-visible { box-shadow: var( --_holo-focus ); }
`;holoTokens declares the aliases the others read — include it once per component. Import holo instead for the whole vocabulary.
Two rules the guards enforce (tests/vitest/holo-layer.test.ts, tests/vitest/component-token-reachability.test.ts):
-
Never declare a
--os-ui-*name on a bare:host. A property declared on the host beats anything it would inherit, so it kills the palette's and every theme's declaration of that name. Read the public token into a private--_aliasinstead. -
Comments inside a
css`` template cannot contain backticks. The template is a JS template literal; a backtick in a CSS comment terminates it and the file stops parsing.
import { OsLog, type OsLogRowRenderer } from 'openstation';Not every class in the tables above is importable from 'openstation'. The package exports map exposes the entry point (src/public-api.ts) plus two subpaths — openstation/activity and openstation/global (ambient types); the entry point re-exports the Stable kit: OsAvatar, OsBadge, OsButton, OsCheckboxLabel, OsCluster, OsCode, OsColorField, OsDisplay, OsEmptyState, OsGrid, OsIcon, OsKey, OsLog, OsMenu, OsMenuItem, OsPanel, OsRangeField, OsSection, OsSegment, OsSegmented, OsStack, OsStep, OsSteps, OsSwatch, OsSwatchGrid, OsTab, OsTabChip, OsTabs, OsTextarea, OsToast, OsToastContainer, OsWindowButton. If src/public-api.ts and this list disagree, the source wins.
The remaining classes are internal-only for now — any other subpath / source-path import is blocked by the exports map — though their tags still work wherever a loaded bundle has registered them. The class import is for type-checking, subclassing, or programmatic instantiation; importing anything from 'openstation' also registers every tag as a side effect. See use-from-a-plugin.md for the local-install workflow.
Every class has a static help = { … } block with full props / slots / events / examples / status. The OpenStation Preferences → Components tab iterates OS_COMPONENT_TAGS and renders these descriptors live; that's the authoritative per-component reference. The table above is a directory; the static help block is the manual.
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
- 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