Bar widgets
Reference for the bar-only surface: the taskbar store and the render list, 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 covers the taskbar store and the render list, the
notification-area mirror, the start-menu tap and the per-app context menu.
The same taskbar + bar-kind gate also guards the open-windows and
pinned-app surfaces (dd.windows.*, dd.pins.*) and the host-drawn preview
flyout (dd.bar.flyout.*), which the store drives for you.
Most bars never call any of it directly: the
bar components
(<dd-apps>, <dd-bar-button>, <dd-tray-caret>) carry the behavior, and a
bar differs in its CSS.
Where the bar docks
A bar's manifest names the edges it can dock to (bar.edges, default
["bottom"]; see the manifest reference).
"left" and "right" make a vertical bar: the window is the full display
height and bar.thickness wide. A bar that lists several edges gets a
Side choice in the bar's own settings dialog (right-click the bar; the
Placement group at the top). Your document learns the docked edge
from ctx.bar.edge (the same value reaches a bar's pane; a <dd-bar>
segment reads it itself, dd.bar.store().edge carries it as a signal) and
mirrors its layout from it. Every host popup
that hangs off the bar (previews, the tray list, the app menu, a pane)
opens beside the strip on a side bar, so anchors carry { x, y, w, h }:
pass the element and the SDK measures it.
The same Placement group offers Auto-hide for whichever bar is active:
the host reserves no space, slides the bar off its edge while the mouse is
away, and brings it back when the cursor touches that edge. The slide is
the host's; your document neither moves nor needs to know. The manifest
tunes the feel through the optional bar.autoHide block: hideDelay (ms
the cursor must stay away, 0 to 2000, default 150), revealBand (logical
px on the screen edge, 1 to 8, default 2) and slide (ms, 0 to 600,
default 140). The user switches auto-hide on; you set how it behaves.
The same group has a Size slider (50% to 200%, one value per bar): the
host scales the window's thickness, and with it the reserved strip, so a
document that derives every size from its viewport (100vh on a bottom
bar, 100vw on a side bar, or a unit like --u: calc(100vh / 128))
scales whole. Literal pixel sizes and theme font-size tokens do not; treat
the manifest thickness as the design size, never as a constant.
What stays native: the Windows taskbar is hidden, not removed, and Windows
keeps reporting it to other apps. Beside a side bar the Start menu
(dd.bar.openStartMenu) still spawns at the bottom edge, where Windows
puts it, and third-party apps that place their toasts above "the taskbar"
(Steam, for one) keep a 48 px gap at the bottom. Windows' own notifications
use the work area and land right. A bar that wants Start beside itself
draws its own start pane with the popout mechanism below.
A pane of your own
A bar may declare the same popout block as a grid widget (entry only,
no max) and open it with the same dd.popout.open from a click. The host
renders the pane document beside the strip in its own window, with your
grants, ctx.popout.side pointing away from the bar and ctx.bar.edge
set; size is CSS px capped to the display and prefer is ignored. The
pane closes on Escape, a press outside it and its button, a fullscreen
app, a reload, a tray click or a taskbar change, and it never takes
keyboard focus (pointer-only). dd.popout.send and onMessage work
between the two documents. The bar-window-only methods on this page stay
with the bar document; the taskbar streams (windows.changed,
pins.changed, systray.changed) reach the pane too. The
popouts guide has the details.
Every other grant works too
taskbar unlocks the bar-only surface on this page. The rest of the
permission set (time, system, media,
audio, notifications, http and the others) is declared and gated on a
bar exactly as on a grid widget, and the streams reach the bar document on
every display, so a taskbar can carry a clock, a now-playing chip, vitals
meters or an audio-reactive surface beside its apps. Audio capture stays
demand-driven: the active bar counts as a visible audio widget since app
0.3.0, and its anyAudio setting is read from the bar's settings.
Where bar widgets live
Bar widgets load from the taskbars folder, next to the widgets folder
(default Documents\DeskDash\taskbars). The split is enforced both ways: a
kind: "bar" manifest in the widgets folder is quarantined with a pointer at
the move, and only bar widgets load from the taskbars folder. Users pick the
active taskbar in the marketplace's Taskbars section (one bar runs at a
time, so installed taskbars offer Use instead of Add), and they edit its
settings from the bar's own right-click menu.
Publishing taskbars
Taskbars ship through the same marketplace pipeline as widgets. On the
submit page, pick Taskbar at the top and upload
the folder as a zip; the manifest must declare kind: "bar" (with its bar
block and minAppVersion of at least 0.3.0), and the selection is checked
against the zip so a mix-up gets a clear error instead of a mislabeled
listing. Taskbars have no store categories; everything else (screenshots,
features, the long description, the hands-on review) works exactly like a
widget submission. Published taskbars appear on the store's
Taskbars shelf and in the app's marketplace under
Taskbars, where installing one offers Use right away.
dd.bar.store
dd.bar.store(): BarStoreThe headless taskbar state, one lazy instance per bar document; the first
call arms the host streams. Signals (dd.ui.signal, read .value inside
an effect, .peek() outside): windows and pins (the host snapshots),
entries (the render list below), flyout ({ open, key }, from
bar.flyout.changed only), pinsEnabled, 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). ready resolves after the
seed, 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 (pins.list is never called while off); setFlyoutStyle
sets the style every open sends; menuSubject(entry) is the app-menu
subject. 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.entries
dd.bar.entries(windows: OsWindow[], pins: PinnedApp[]): BarEntry[]The render list every taskbar wants, pure: 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. The
store's entries signal is exactly this over its snapshots.
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.
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 | { x: number; y?: number; w: number; h?: number }): 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 click, Escape, a row action, or the bar going away.
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 | { x: number; w: number };
}): 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.pins.onChanged.
A pin's appKey is its own id, 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 and a pin id are the same kind of 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 (Taskbar settings, Settings, Edit layout, Quit).
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.