Building a taskbar
What a taskbar is, what the host does for it, and a first bar from three tags: the folder, the manifest, the edge, input by segments, previews, a pane, settings and publishing.
A DeskDash taskbar is a widget with "kind": "bar". The same folder of
plain HTML, CSS and JavaScript, the same sandbox, the same dd SDK, the
same components and tokens as a desktop widget.
What changes is the surface: instead of a tile on the grid, the widget
renders as the taskbar, docked to a screen edge, and the Windows taskbar
steps aside while it runs.
What the host does for you
The split of ownership is the whole model, so read it first.
The host owns the window. It creates one bar window per display, docks it to the edge your manifest names, reserves the strip with the OS so maximized windows stop above it, hides the native Windows taskbar and brings it back on quit, on a crash, or when the user turns the bar off. Your document never moves, never resizes and never knows about any of it.
The host owns the data. One ordered strip of pinned and running apps, the same for every bar on every display; the open windows, already cut to what each bar shows; the pins, which live in the app config and survive a bar switch; the notification area, read back from Windows so a bar can offer it. A bar renders from these streams and never keeps an order or a pin list of its own.
The host owns every popup. Window previews, the per-app right-click menu, the tray list and your own pane are separate host-drawn windows that open beside the strip. A bar says when and for which app; the host draws, places, refreshes and closes.
You own the HTML and CSS. That is where two taskbars differ: the dock is a centered pill with a magnification wave, the rail is a tall glass column on a side edge, aurora is a full-width bar with a clock and a now-playing chip. Behind all three sit the same four components and the same store.
One bar runs at a time. The user picks it in the marketplace, and it shows
on every display, or on the one they choose in Settings, Taskbar (app
0.3.4+). With a bar on every display, Show taskbar apps on in the same
page picks which bar lists a window, like the Windows setting of that
name (app 0.3.11+). The host applies it to the window list, so your bar
needs no code for it. Bars have no instances: the document's role is "bar" and its
instanceId is the sentinel "__bar" on every display.
Start from a folder
Bars load from the taskbars folder, next to the widgets folder (default
%LOCALAPPDATA%\com.deskdash.desktop\content\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. The two
folders share one id namespace.
The scaffolder in developer mode makes desktop widgets, so a taskbar starts by hand: copy one of the bundled bars from the taskbars folder, or create a folder with the files below. Add to a folder... on the Developer page writes the dev kit into it.
my-bar/
├─ manifest.json # kind, the bar block, permissions, settings
├─ index.html # the bar document
├─ style.css # the look
└─ widget.js # optional: only what the look needsThe manifest of the bundled dock, which is a complete taskbar:
{
"id": "dock",
"name": "Dock",
"version": "0.4.5",
"apiVersion": 1,
"description": "A macOS-style floating dock ...",
"author": "deskdash",
"kind": "bar",
"bar": { "thickness": 128, "reserve": 68 },
"settings": ["pins", "systray"],
"permissions": { "taskbar": true }
}Four things are bar-specific. kind makes it a taskbar. The bar block
sizes the window (the manifest reference
has every field and range). The taskbar permission unlocks the bar-only
surface: windows, pins, the strip, previews, the tray and the app menu
(see permissions). The two
presets, pins and systray,
give the user the "Show pinned apps" and "Show the system tray" toggles
that the components apply on their own. There is no minAppVersion: the
store stamps it when you publish (older apps do not know kind, and the
strip and drag to reorder are app 0.3.5+); the
feature floor
explains how.
Three tags are a taskbar
This is the dock's document, with its comments and ids trimmed:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/__sdk/dd-base.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<dd-bar place="center" reach="35">
<dd-panel row>
<dd-bar-button action="start" label="Start"></dd-bar-button>
<dd-apps settings="pins" indicator="dots" sortable></dd-apps>
</dd-panel>
<dd-tray-caret settings="systray" hidden></dd-tray-caret>
</dd-bar>
<script src="/__sdk/widget-sdk.js"></script>
</body>
</html>Every behavior a taskbar has is in those tags, with no widget code:
<dd-bar>is a segment: the part of the window that takes the mouse. Everything outside it clicks through to the desktop or the maximized window beneath.placesays where along the edge it sits,reachadds hover headroom above the content.<dd-bar-button action="start">is the start button. It draws the four-pane logo itself and taps the Windows start menu on a click.action="quick-settings"andaction="notifications"open Windows' own Quick Settings and notification center the same way (app 0.4.2 or newer), so your users keep the sound device, Bluetooth and Night light with no code of yours.<dd-apps>is the app strip: one<dd-app>per row of the host's strip, pins and running apps in the user's order, windows merged per app. A click launches, focuses, minimizes or opens the preview flyout; a right-click opens the host's per-app menu;sortable(app 0.3.5+) makes a press-and-drag reorder the strip.settings="pins"wires the "Show pinned apps" toggle.<dd-tray-caret>is the notification-area caret. It stays hidden until the tray has icons and the setting is on, and a click opens the host-drawn tray list.settings="systray"wires the toggle.- Not in the dock, but one tag away:
<dd-notification-bell>(app 0.4.2 or newer) opens the host's own notification center beside your bar, with the count as its badge, instead of Windows' one in the corner.settings="notificationCenter"wires a "Show notifications" toggle.
The dock's widget.js exists only for the magnification wave. A bar that
wants a different look edits style.css; the components are the box, so
size them, animate them and style their parts from widget CSS. The
components guide has the general rules and the
reference lists every
attribute, state and part.
See it
Open the marketplace (the tray icon, or Add widgets... in its menu), switch to Installed, then Taskbars: your folder is listed with a Use button, because one bar runs at a time. Use it, and the Windows taskbar gives way to yours.
Right-click the bar for its menu. Taskbar settings... opens the bar's
own settings dialog: the Placement group first (Side, when the
manifest lists several edges; Size, 50% to 200%; Auto-hide; Let windows go
behind the bar), then your own settings, live-previewed as they change.
The same dialog has a Style tab (also Taskbar style... in the menu):
the user's --dd-* overrides for the bar alone, background, border, fonts
and colors, previewed live through theme.tokens like a theme switch. Your
CSS sees a different var() value and nothing else changes.
Change taskbar... goes back to the Taskbars section. After an edit,
Reload widgets from the tray reloads the bar too.
Placement is the user's. The manifest tunes what they can pick: edges
lists the sides the bar supports, reserve and allowOverlay decide
whether windows may fill behind it, and the optional autoHide block sets
the feel of auto-hide (the delay, the reveal band on the screen edge, the
slide time). The user switches auto-hide on; you say how it behaves. The
slide is the host's: your document neither moves nor needs to know, and
the reserved strip goes to zero while auto-hide is on.
Lay out along the edge
A bottom bar is the full display width and thickness tall. A left or
right bar is the full display height and thickness wide, and every host
popup opens to its side instead of above it. Your document learns the edge
from ctx.bar.edge in the init context
(the same value reaches a bar's pane); a <dd-bar> segment reads it on its
own and lays out as a column on a side edge, and dd.bar.store().edge
carries it as a signal. The rail mirrors its layout with one attribute on
the body:
const ctx = await dd.ready;
document.body.dataset.edge = ctx.bar?.edge ?? "bottom";Size against the viewport, not in fixed pixels. The user's Size
slider (app 0.3.1+) scales the window's thickness, and with it the
reserved strip, so a document whose sizes derive from 100vh (a bottom
bar) or 100vw (a side bar) scales whole, while literal pixel sizes and
theme font-size tokens do not. The bundled bars use one unit:
:root {
--u: calc(100vh / 128); /* the manifest thickness, in design px */
--icon-rest: calc(var(--u) * 52); /* an icon at rest */
--pill-h: calc(var(--u) * 72);
}Treat the manifest thickness as the design size, never as a constant.
Colors, radii and type come from the design tokens
like everywhere else.
Shorter bars: Length and Alignment
The user can also make a bar shorter (app 0.4.5+): Length is the full
edge, fit to content, or 30% to 95% of the edge, and Alignment puts a
shorter bar at the start, the center or the end. The window and the
reserved strip stay full edge; only your <dd-bar> segments move. The SDK
lays them out inside the span on its own: fill fills it, the end places
sit at its ends, and center sits at its middle. Under fit to content,
fill shrinks to its content, and fill and center move to the chosen
end, so a fitted bar or dock can sit on the left. Build with segments and
this works with no code.
The user sets Size, Length and Alignment in the taskbar's settings, or drags them in Edit taskbar... (the taskbar menu, app 0.4.5+): grips on the bar change its Size and Length, and a drag along the bar moves it. A drag resizes your window in steps and moves your segments many times a second, so lay out from the viewport and the span, never from a size you measured once.
Until the user picks a Length, a bar has its natural one, which the SDK
reads off your segments: a bar whose segments are all center (a floating
dock) is fit to content, every other bar is full edge. The dialog shows
that as the default. Two shapes get extra help:
-
A dock (one
centersegment): under full edge or a share of the edge, the SDK stretches the segment over the span. Your CSS decides how the surface fills that box; the Dock lets its pill grow::root[data-dd-bar-length="full"] #dock, :root[data-dd-bar-length="percent"] #dock { flex: 1 1 auto; width: auto; } -
Split segments (say left, center and right): the SDK measures them and places them as one group, so they never overlap. A share of the edge never gets narrower than the segments and their gaps, fit to content packs them side by side, and the center segment moves aside before it touches a neighbor.
What you may want to adjust, all in CSS:
-
A
fillbar whose middle grows (an app strip withflex: 1) spills into its side zones when the bar is short. Clip it along the edge while the bar is a share of the edge, and let it take its content size under fit::root[data-dd-bar-length="percent"] dd-apps { overflow-x: clip; } :root[data-dd-bar-length="fit"] dd-apps { flex: 0 1 auto; } -
A surface squared off to the screen corners should round the corners away from the edge (with
--dd-radius-lg, like the bundled bars) and close its sides once it no longer meets them (data-dd-bar-lengthisfullotherwise).
ctx.bar.length ("auto" until the user picks one), percent and align
carry the values, and data-dd-bar-align sits on <html> beside
data-dd-bar-length (the resolved length, never auto). Your own
left, right or width on a segment overrides the span.
Input: segments, not flags
A bar has no interactive flag. The window takes the mouse natively
everywhere: plain CSS :hover and click handlers just work, and for a
solid full-width bar that is the whole story.
A bar with empty space (a centered pill, split segments) wraps its visible
content in <dd-bar> segments. Each segment's box captures the mouse;
everything outside every segment clicks through. Two consequences:
- Pointer events outside the segments never reach your document, so hang hover waves and leave timers on the segment, not on the body.
- A
reservesmaller thanthicknessleaves transparent headroom above the strip where maximized windows fill in. Use it only together with segments; without them that band is an invisible click-eater. Give the segmentreachso hover animations that rise into the headroom stay hoverable.
The mechanics, and the rules for marking regions after layout, are in the interactivity guide.
Previews, the app menu and the tray are the host's
A stacked app's click opens the preview flyout: window titles and live
snapshots, drawn by the host beside the strip. previews="hover" on
<dd-apps> opens it after a dwell instead, the native taskbar feel. The
look is tunable through a curated style (preview width, radius, gap,
surface and border colors, title and close button), set once with
dd.bar.store().setFlyoutStyle(style); everything else about the flyout
is the host's, including when a card shows the app's icon instead of a
snapshot. The flyout reference
has the ranges and the fallbacks.
A right-click on an app button opens the host's per-app menu: launch, pin
or unpin, close. <dd-app> tags itself so the SDK raises it; a bar that
draws its own buttons stamps the same tag by hand
(dd.bar.appMenu). Never
draw an app menu or keep a pin list of your own: the actions are host
privileges, the pins belong to the app config, and a bar is too thin to
hold a menu anyway. Untagged space raises the bar's own menu.
The tray caret opens the host's tray list. A bar sees a count and the open state, never the icons, so the caret's pressed look follows the dd.systray stream. Tell your users what the list shows: every app's icon, hidden ones dimmed, and DeskDash's own; the Windows volume, network and battery icons stay with Windows.
A pane of your own
A bar may declare the same popout block as a grid widget, without
max, and open it from a click. The host renders the pane document
beside the strip, in its own window, sized to your CSS px and capped to
the display. The rail's games button is one:
<dd-bar-button action="pane" pane-w="620" pane-h="620" label="Games">
<dd-icon src="gamepad.svg" tint></dd-icon>
</dd-bar-button>"popout": { "entry": "games.html" },
"permissions": { "taskbar": true, "games": true }The pane gets the bar's grants and ctx.bar.edge, closes on Escape, an
outside press, a fullscreen app or a taskbar change, and never takes
keyboard focus, so design it pointer-only. The taskbar streams reach the
pane too. What each document may call, from app 0.3.7:
- Bar window only: the flyout, the app menu, the tray list, the strip
move, the start-menu tap,
dd.popout.openand the region reporting. - Bar window or pane, under the cursor-inside gate (the bar window, or the
pane and its anchor button):
dd.settings.open(it opens the Taskbar settings window),dd.links.open,dd.dialogs.pickDesktopItemsanddd.power.run. Older apps refuse these from a bar outright. - Both, with no gate: the streams,
dd.bar.strip.list, thedd.windowsreads anddd.pins.list.
From app 0.5.1 all of these work in edit mode too, because edit mode never
takes a bar's clicks. Older apps refuse the pane and every gated call in
edit mode with PERMISSION_DENIED, so leave edit mode before you test them.
Details in popouts, on a taskbar; the games library behind that pane is dd.games.
Settings without an instance
Bars have no instance, so the active bar has one settings home in the app
config, served by dd.settings.get merged over your manifest defaults,
and one dialog: the bar's right-click Taskbar settings.... Values
live-preview as they change, Save persists, any other close replays the
stored values. Everything in the settings guide
applies, with two exceptions. A bar manifest may not declare an explicit
consent-bearing field (secretRef, filePath, instances): the bar dialog
never offers those types, so the app refuses the manifest at load, the upload
refuses it and the CI check fails it (app 0.3.7 or newer); preset names are
exempt, so panelStyle still works minus its image option. And the two bar
presets, pins and systray, exist for bars only.
Storage without an instance
dd.storage on a bar
addresses one store that belongs to the bar widget, not to an instance. The
bar document on every display and the bar's pane read and write the same
keys, under the same limits as a grid widget. Another bar never sees it, and
it is still there when the user switches back to yours.
Nothing announces a write. The copy of your bar on another display learns of it the next time it reads, so read the value at the moment you need it instead of caching it at boot.
This needs app 0.4.1 or newer. An older app rejects every storage call a bar
makes with INVALID_PARAMS, and dd.weather.geocode with it, because its
cache is a storage read.
Every other grant works
taskbar unlocks the bar-only surface. 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 the bar runs on. Aurora is the worked example: a clock, a
now-playing chip, vitals rings and an audio-reactive surface beside its
apps, plus a transport deck in its pane. One note for audio: capture
stays demand-driven, the active bar counts as a visible audio widget, and
its anyAudio setting is read from the bar's settings.
What stays native, and the limits
The Windows taskbar is hidden, not removed, and Windows keeps reporting it
to other apps. Your users will see three things beside a side bar or a
short bar: the Start menu opens where Windows puts it, at the bottom edge,
left or center per their Windows setting, and Quick Settings and Windows'
own notification center in the bottom right corner (the
<dd-notification-bell> center opens beside your bar instead); third-party apps that place
their toasts above "the taskbar" (Steam, for one) keep a gap at the
bottom; and Explorer re-shows the hidden taskbar on every Start open and
work-area change, which the host re-hides at once, so a flash is the most
anyone sees. A bar that wants Start beside itself draws its own start pane
with the pane mechanism above.
Elevated windows (Task Manager, an elevated terminal, some installers) sit
behind a Windows boundary that drops messages from ordinary apps. Focus
still works through a fallback; minimize is refused and the click does
nothing; the close request from a preview card or the app menu is dropped;
the preview shows the icon tile. Previews also fall back to the icon tile
for minimized windows and for windows that draw straight to the GPU
(games, some browsers and players). None of this is yours to fix, and the
streams never desync over it: drive every indicator from
dd.windows.onChanged, never from a click.
Publish
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 the choice 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 review) works exactly
like a widget submission, see publishing.
Published taskbars appear on the store's
Taskbars shelf and in the app's marketplace under
Taskbars, where installing one offers Use right away.
Where to go next
- Bar widgets reference: the store, the strip, the windows, the pins, the flyout, the tray and the app menu.
- Components: every attribute, state and part of the four bar components.
- A taskbar that feels native: the corner, the order, the motion.