Bar widgets
Reference for the bar-only surface: the taskbar store and the strip, the open windows, the pinned apps, the preview flyout, the system tray mirror, the start-menu tap and the per-app context menu, gated by the taskbar permission.
Some methods exist only for a kind: "bar" widget that also holds the
taskbar permission. The bridge refuses the
whole surface for a regular desktop widget before permissions are even read,
so "a desktop widget can never see your open windows" stays a one-line
guarantee.
This page is the reference. Building a taskbar explains what a bar is, what the host does for it, and how to make one.
Most bars never call any of it directly: the
bar components
(<dd-apps>, <dd-bar-button>, <dd-tray-caret>) carry the behavior on
dd.bar.store, and a bar differs in its CSS. The primitives
under them stay public for a bar that draws its own buttons: the strip (the
host-owned order), the windows, the pins, the preview flyout, the tray
mirror, the start-menu tap and the app menu.
Two rules hold everywhere on this page. Every id is an opaque host token: a
stale one rejects with NOT_FOUND, and the fix is always to re-list from
the matching stream. A popup anchor is an element (the SDK measures it) or
a rect in the bar document's CSS px; y and h matter beside a side bar,
where every popup opens to the side of the strip.
Types
interface OsWindow {
/** Valid only while the window is in the current list. */
id: string;
title: string;
/** Stable per-app icon key for dd.windows.icons; empty when the host
* could not read the process. */
iconKey: string;
/** The id of the strip row that claims this window (a pin's id, or the
* id of the row a running app got), stamped by the host before either
* list goes out and fixed while the window is listed. Empty only on a
* host older than 0.3.5. */
appKey: string;
focused: boolean;
minimized: boolean;
/** Stable display id of the window's monitor (its restore monitor while
* minimized), for a bar that filters per display. */
displayId: string;
}
interface PinnedApp {
/** The only handle dd.pins.launch accepts; stable across rescans and
* across unpin and re-pin of the same app. */
id: string;
name: string;
/** Equals id: the host stamps it onto every window the pin claims. */
appKey: string;
/** Shares the windows icon space: a pinned and running app is one cache entry. */
iconKey: string;
}
interface StripRow {
/** The row's identity everywhere: the appKey, the iconKey, the id that
* pins.launch and strip.move take. Survives pin, unpin and re-pin. */
id: string;
/** A pin (stays when its windows close) or a transient row (drops a
* moment after its last window closes). */
pinned: boolean;
/** The pin's display name; empty for a transient row (show the window title). */
name: string;
/** Equals id. */
iconKey: string;
}
interface BarEntry {
key: string;
/** The running windows; empty for a launcher. */
wins: OsWindow[];
/** The pin behind the entry; null for an unpinned running app. */
pin: PinnedApp | null;
}
interface BarFlyoutStyle {
/** Preview width in CSS px, 120 to 320; height follows 16:9. */
previewW?: number;
/** Panel corner radius in px, 0 to 24. */
radius?: number;
/** Gap between cards in px, 0 to 24. */
gap?: number;
/** Panel background, #rrggbb or #rrggbbaa only. */
surface?: string;
/** Panel border color, #rrggbb or #rrggbbaa only. */
border?: string;
showTitle?: boolean;
showClose?: boolean;
}
/** A popup anchor by hand: CSS px of the bar viewport. */
type BarAnchor = { x: number; y?: number; w: number; h?: number };One key ties the three lists together. A strip row's id is the appKey
the host stamps onto every window the row claims, the iconKey for
dd.windows.icons, the id dd.pins.launch
takes for a pinned row, and the id dd.bar.strip.move
takes. Attaching windows to rows is a plain equality, and a bar never keeps
an order of its own.
dd.bar.store
dd.bar.store(): BarStoreThe headless taskbar state, one lazy instance per bar document. Signals
(dd.ui.signal, read .value inside an effect, .peek() outside):
strip (the host's ordered rows, see dd.bar.strip) and
windows (the host snapshots), pins (the pinned rows as PinnedApp, a
projection of the strip), entries (the render list: the strip with the
windows attached, one entry per row that has something to show, in strip
order, or in the local order during a drag or a move in flight), flyout
({ open, key }, from bar.flyout.changed only), pinsEnabled,
sorting, lowMotion (low-spec host or reduced motion), edge; per key,
icon(iconKey) (the data URI, fetched in batches on first read; a key the
host has no icon for stays undefined and is never asked again) and
launching(key). entryByKey(key) returns the entry behind a key, or
undefined. ready resolves after the seed (the strip first, then the
windows), with every listener already registered.
Actions: activate(key, anchorEl) is the click tree (a launcher launches
once, behind a lock that holds until its window lands or 6 s; a single
window focuses, or minimizes when it is the focused one; a stack toggles the
preview flyout anchored on the element), with launch, openFlyout,
closeFlyout and toggleFlyout as its pieces; hoverEnter(key, el) and
hoverLeave() run the hover dwell when configure({ previews: "hover" })
is set (a sweep opens nothing, a switch while open waits the same dwell,
leaving a button closes nothing); pointerEntered() and pointerLeft()
are the debounced advisory close for the bar segment; enablePins(bool) is
the pins gate (whether pinned launchers render; the host keeps its pins
either way); beginSort(key), previewOrder(keys), move(key, before)
and endSort() are a drag's four moments (the flyout closes and hover
pauses at beginSort, previewOrder keeps the pointer's order across host
emits, move shows the drop at once and sends bar.strip.move, the host's
next emit confirms it or a rejection rolls it back within a second,
endSort is a release without a move); setFlyoutStyle sets the style
every open sends; menuSubject(entry) is the app-menu subject.
configure(opts) takes previews ("click", the default, or "hover"),
dwell (ms of continuous hover before a preview opens, default 400) and
flyoutStyle. hooks is the seam the components use to raise their
cancelable events; leave it alone.
The <dd-apps> / <dd-app> / <dd-bar-button> components render from it
and hold no behavior of their own. A bar that draws its own buttons reads
and calls the same thing.
dd.bar.strip
The strip (app 0.3.5+, declare "minAppVersion": "0.3.5") is the
host-owned order of the taskbar: ONE list of rows, where a row is a pinned
app or a running app nobody pinned, the same for every bar on every
display. It is the user's order. Pins keep their place across restarts
(they live in the app config); running rows keep theirs while they run and
append at the end when they first appear, exactly like the Windows taskbar.
Pin and unpin happen in place: a running app keeps its slot when the user
pins it, a pinned app keeps its slot when unpinned while it runs, and an
unpin with no window drops the row.
Every window in dd.windows.list carries its row's id as appKey, stamped
by the host before either list goes out, so attaching windows to rows is a
plain equality and a bar never keeps an order of its own.
dd.bar.strip.list()
dd.bar.strip.list(): Promise<{ items: StripRow[] }>The rows in strip order. Seed with this, then follow onChanged. A row
with pinned: false and no window in the window list is in the host's
short grace after its last window closed (its id stays valid for a
relaunch): render nothing for it. A bar's pane may call this too.
dd.bar.strip.move()
dd.bar.strip.move(id: string, before: string | null): Promise<void>Moves row id in front of row before, or to the end of the strip when
before is null. Any row goes to any slot: a running app between two
pins, a pin after a running app. This is the one write a bar has into
host-owned state, so it is gated like the app menu: the bar document only
(never a pane), never in edit mode, and only while the cursor is over the
bar (a drop lands under it). Only host-issued ids cross; a stale id or
before rejects with NOT_FOUND (re-list on onChanged). The result comes
back through onChanged to every bar, and through dd.pins.onChanged when
the pinned order changed. The store does this for you: <dd-apps sortable>
turns a drag into move, with the drop shown at once and the stream
confirming it.
dd.bar.strip.onChanged()
dd.bar.strip.onChanged(cb: (p: { items: StripRow[] }) => void): () => voidChange-driven full snapshot: a row added (an app nobody pinned opened its
first window) or dropped (its grace ran out), a pin made or removed, a move
(yours or another bar's, on any display). Attaching windows to rows never
emits here; dd.windows.onChanged carries that. Reaches a bar's pane too.
dd.bar.entries
dd.bar.entries(windows: OsWindow[], pins: PinnedApp[]): BarEntry[]The pure two-list merge, for a bar that reads dd.windows and dd.pins
itself: pins first in pin order, each merged with its running group when
the host stamped the pin's id onto the windows (appKey equality), a
launcher otherwise; then the unpinned running groups in the host's
first-seen order. Windows without an appKey stay their own entry. A merged
entry and its launcher share one key, so one DOM node can carry launcher,
running, launcher across a launch and a quit. It carries no user order:
the store's entries signal renders from the strip instead. The shapes are
in Types.
dd.windows
The open top-level OS windows. Needs taskbar on a bar-kind widget; a
regular widget's call is refused before its permissions are read.
dd.windows.list()
dd.windows.list(): Promise<{ windows: OsWindow[] }>Every open top-level window, unstacked, in first-seen order: the order is
stable across focus changes and new windows append, so the list never
leaks z-order and buttons never jump. Each window's appKey is the id of
the strip row that claims it (by AppUserModelID when the window carries
one, else by exe; every UWP window shares one host process, which is why
exe alone would misgroup them). A new window is held out of the list for
up to about 300 ms while that identity resolves, so the key never changes
under a listed window. Seed with this, then follow onChanged. The store's
entries signal is this list attached to the strip.
dd.windows.focus()
dd.windows.focus(id: string): Promise<void>Restores the window if it is minimized, then asks Windows for the
foreground. Fire and forget: a refused foreground attempt still resolves
and is only logged host-side (the OS foreground lock; an elevated window,
where the host falls back to the Alt-Tab switch path, which works). Drive
your focus indicator from onChanged, never from the click, and a refusal
can never desync it.
dd.windows.minimize()
dd.windows.minimize(id: string): Promise<void>Minimizes the window; the indicator update arrives through onChanged
when focus moves. Windows refuses the request for an elevated window (Task
Manager, an elevated terminal) and the click then does nothing. That is
not a trap: such a window can only be minimized from its own caption, and
the next click on its button restores it through focus.
dd.windows.icons()
dd.windows.icons(keys: string[]): Promise<{ icons: Record<string, string> }>Icon PNG data URIs (up to 96 px) for up to 64 iconKeys from the current
list, extracted on demand and cached per app for the session. Keys the
host cannot resolve are simply absent: keep a glyph fallback and do not ask
again (the miss is cached too). Icons are never inlined in the list or the
stream, so cache by iconKey and fetch only unseen keys; the store's
icon(iconKey) signal does exactly that, in batches.
dd.windows.onChanged()
dd.windows.onChanged(cb: (p: { windows: OsWindow[] }) => void): () => voidChange-driven full snapshot: a window opened or closed, focus moved, a title changed, minimize state flipped, a window changed monitors. Nothing fires at a fixed rate and there is nothing to poll. This is the driver for a taskbar's focus indicator and button row.
dd.pins
DeskDash's own pinned apps: one host-owned list, kept in the app config and
shared by every bar widget, so switching bars, reloading or reinstalling a
bar never changes it. Pins are made and removed through the host's
app menu, never by a widget, and moved through
dd.bar.strip.move. On the first run the host imports the
Windows taskbar's own pins once, read-only. Same gate as dd.windows.
dd.pins.list()
dd.pins.list(): Promise<{ pins: PinnedApp[] }>The pins in pin order: the pinned rows of the strip, in strip order. A
pin's appKey is its id, stamped onto every window it claims before the
window list goes out, so a pin and its running windows share one key from
the first frame either exists.
dd.pins.launch()
dd.pins.launch(id: string): Promise<void>Launches the pinned app through the host's own launcher for it (a packaged
app's AppsFolder entry, the app's Start menu shortcut, or its exe; nothing
crosses the bridge). The window it opens arrives already carrying this
pin's appKey, so it merges on the dd.windows.onChanged that announces
it. Always a new-instance request: correlate running windows through
appKey and decide yourself whether a running pin's click should focus
instead (the store does). Rejects NOT_FOUND for a stale id and for a pin
whose launcher and exe are both gone; a launch the OS refuses rejects
INTERNAL. Rate limited to 5 launches per 10 seconds.
dd.pins.onChanged()
dd.pins.onChanged(cb: (p: { pins: PinnedApp[] }) => void): () => voidChange-driven full snapshot: fires only when the pin list itself changed,
a pin made or removed from the app menu, or the pinned order moved. A pin's
appKey is fixed, so launching or quitting an app never emits here;
dd.windows.onChanged carries that.
dd.bar.flyout
The host-drawn preview flyout: window titles, live snapshot previews and a
close button per card, for one app group at a time. You decide when and for
which group; the host does everything else: where it goes, what it shows,
when it closes. Bar document only (a pane cannot open it). The store and
<dd-apps> drive it for you (previews="click" or "hover"); the methods
below are the surface underneath.
dd.bar.flyout.open()
dd.bar.flyout.open(opts: {
key: string; // your group tag, up to 64 chars; echoed on onChanged
windowIds: string[]; // 1 to 32 ids from the current window list
anchor: Element | BarAnchor;
style?: BarFlyoutStyle;
}): Promise<void>Opens the flyout for a group, or retargets it while open (the instant
switch, no flicker). Stale ids are pruned silently and a fully stale group
just closes. The flyout centers on the anchor beside the strip (above a
bottom bar, to the side of a left or right bar), clamped to the display,
up to 8 cards. style is the curated author styling: every knob defaults
to the active theme's tokens, and colors are hex only, never a CSS
expression.
Fire and forget: the content builds host-side, so drive your hover machine
from onChanged, never from this promise. A click on a card focuses its
window (restores it if minimized, never minimizes) and closes the flyout;
the card's X posts a graceful close request to the app, which may prompt
to save.
What a card shows. Previews are snapshots the host takes from the window, refreshed about once a second while the flyout is open. A card falls back to the app's icon tile when there is nothing to capture: a minimized window, a window that draws straight to the GPU (games, some browsers and players), or an elevated window, which refuses capture. None of that is yours to fix; design the icon tile to look intended.
dd.bar.flyout.close()
dd.bar.flyout.close(): Promise<void>Advisory close, meant for pointer-leave: the host holds a short grace (about 300 ms, so the pointer can cross to the panel) and ignores it while the cursor is inside the flyout itself, whose own leave then closes it. The host also force-closes on its own: edit mode, a reload, a fullscreen app, the app menu or a pane opening, the bar going away or crashing, an emptied group, and a no-hover backstop of about 10 seconds. A stranded flyout is never the author's problem.
dd.bar.flyout.onChanged()
dd.bar.flyout.onChanged(cb: (p: { open: boolean; key: string | null }) => void): () => voidThe flyout's real state, on every open, switch and close, whoever caused
it. key echoes the tag from open and is null once closed. This is the
only truth for open state and the driver for switch-while-open hover
behavior; never assume the state from your own calls.
dd.systray
The Windows notification area ("system tray"), mirrored by the host so a bar can offer it back once DeskDash replaces the native taskbar. You get a count, never the icons: the rows, and the clicks that reach those apps, live in a host-drawn window that no widget can read or script.
What that list shows, so you can explain it to your users: every app's tray icon, including the ones an app marked hidden (dimmed, because once the native taskbar is gone this list is the only way to reach them), and DeskDash's own icon; Windows' own volume, network and battery icons stay with Windows and are not listed. Rows carry the app's tooltip or its process name, sorted by name, twelve at a time with the rest scrolling.
The easiest way to use it is the <dd-tray-caret>
component with the systray setting preset; the methods below are the surface
underneath it.
dd.systray.subscribe()
dd.systray.subscribe(): Promise<{ count: number }>Arms the host reader (the first call only, so a bar that never shows the tray
costs nothing) and returns the count of shown icons. Seed a caret's
visibility from the count, then follow systray.changed.
dd.systray.open()
dd.systray.open(anchor: Element | BarAnchor): Promise<void>Opens the host-drawn tray list, anchored on your caret. Pass the element and
the SDK measures it. The host owns the list's size, position, dismissal and
every click inside it. Fire and forget: follow systray.changed, not this
promise.
dd.systray.close()
dd.systray.close(): Promise<void>Advisory close (your caret toggled off). The host also closes on its own: an outside press, Escape, a left click on a row, or the bar going away. A right-click on a row keeps the list open so the app's own menu can sit over it, the way the Windows tray overflow does.
dd.systray.onChanged()
dd.systray.onChanged(cb: (p: { count: number; open: boolean }) => void): () => voidFires on every tray change and every open or close of the list, whoever caused it. This is the correct driver for a caret's pressed look: the host closes the list on its own, so follow this, never your own guess.
dd.bar.openStartMenu
dd.bar.openStartMenu(): Promise<void>Opens the Windows start menu (the host taps the Windows key). A second call while it is open toggles it closed, the native behavior. There is no documented API for the menu itself, so the tap is a no-op on the secure desktop or while a held modifier turns it into a chord. Fire and forget: there is no menu state to follow.
dd.bar.appMenu
dd.bar.appMenu(opts: {
appKey?: string; // exactly one of the three
windowId?: string;
pinId?: string;
anchor: Element | BarAnchor;
}): Promise<void>Opens the host-drawn per-app context menu above the bar for one subject:
a merged group by its appKey, a single window by its id (for a window
without an appKey), or a pin by its id (for a pin without one). The menu
shows the app's name (launches a new instance), Pin to taskbar or
Unpin from taskbar, and Close window (or Close all windows for
a group with several). Pins live in the DeskDash config, one list for
every bar widget (switching bars never resets it), and every change comes
back through dd.bar.strip.onChanged and dd.pins.onChanged. Pin and
unpin happen in place on the strip: the app keeps its slot.
A pin's appKey is its own id, the id of its strip row, and the host stamps
that same token onto every window the pin claims before it hands you the
window list. So a pin and its running windows always share one key, from
the first frame either exists, and a group key, a pin id and a strip row id
are the same token.
You rarely call this yourself. <dd-app> tags itself; by hand, tag each
app button with the subject and the SDK's right-click handler does it for
you:
<button data-dd-app="3f9a1c2e">...</button>
<button data-dd-window="131586">...</button>
<button data-dd-pin="9c2f0b4e1d7a6f35">...</button>Re-stamp the tag on every render (ids move as windows come and go). An
element whose own contextmenu handler calls preventDefault() before the
SDK's handler keeps the right-click; untagged space raises
the bar's own menu.
Trust model: you only say which app was right-clicked. Launch, pin, unpin
and close run host-side from clicks inside the host's own menu window, so
the method grants a widget nothing beyond opening the menu, and the
taskbar permission stays a read grant. Honored only while the cursor is
over the bar and never in edit mode; the host closes the menu on Escape, an
outside click, an action, or when the subject disappears. Fire and forget.
The bar's own menu
A right-click on untagged space in the bar document raises the host's
native menu for the bar itself: Change taskbar... (the marketplace's
Taskbars section, the one place a bar is swapped), Taskbar settings...
(the bar's own settings dialog, with the Placement group first),
Settings..., Edit layout and Quit DeskDash. Quit lives here on
purpose: while the native taskbar is replaced, the notification area is
hidden with it, so the bar's menu is the one always-reachable way out, and
a clean quit is also what brings the Windows taskbar back. The SDK forwards
the right-click and suppresses the WebView's default menu; the request
underneath (bar.menu) is bar-window-only and you never need to call it.