Skip to main content
On the desktop shell your app gets a whole window, a mouse and a keyboard. The desktop kit lays it out the way macOS and Windows apps do: a source list down the left, a toolbar over the content, an inspector on the right for the selection, and a status bar along the bottom. It draws the macOS idiom on a Mac and Windows 11’s Fluent idiom elsewhere, including Linux. Use it for the desktop entry of appSurfaces. For a phone-style app that also runs on desktop, StackNavigator adapts to desktop by itself.

DesktopWindow

The window layout. Only the content scrolls; the chrome around it stays put. It hides the shell’s own header (the toolbar takes its place) and leaves room for the macOS window buttons.
ReactNode
The source list down the left, usually a SourceList.
ReactNode
The bar over the content, usually a Toolbar.
ReactNode
The pane on the right for the selection. Pass nothing to close it.
ReactNode
A thin bar along the bottom: counts, sync state.
string
Remember pane widths and whether the sidebar is shown, in localStorage. Leave it out to start fresh every time.
{ initial?, min?, max? }
default:"220 / 170 / 340"
Sidebar width limits in px.
{ initial?, min?, max? }
default:"300 / 240 / 460"
Inspector width limits in px.
Both panes can be dragged wider or narrower. ⌘⌥S / Ctrl+Alt+S hides the sidebar. In a window narrower than 700 px, the sidebar floats over the content instead of squeezing it. Inside the window, useDesktopWindow() returns { os, sidebarHidden, toggleSidebar, hasSidebar }. Outside one, useDesktopOS() still tells you the idiom: 'macos' or 'windows'.

Toolbar

ReactNode
The window’s title: the place or the thing open.
ReactNode
A quieter line beside it, such as “42 items”.
ReactNode
Controls before the title, such as back and forward.
ReactNode
Buttons, a search field and segmented controls, at the trailing end in order.
When your app runs in the desktop shell, the toolbar also carries the shell’s window controls (open in a new window, open in the browser, minimize, close), using the SDK’s window API. ToolbarButton
string
required
Its name: the tooltip, and the text with showLabel or without an icon.
() => void
required
Click handler.
IconName
Button icon.
boolean
Text beside the icon, for the window’s main action.
boolean
The main action, in the accent color.
boolean
A toggle that’s on, such as a view mode.
string
A key combination that presses it, shown in the tooltip, such as mod+n.
boolean
default:"true"
Bind shortcut here. Turn it off when useDesktopCommands already binds the same key.
boolean
Greyed out.
ToolbarSearch takes value, onChange, placeholder? (default “Search”), onSubmit? and shortcut? (default mod+f; false for none). ToolbarGroup (label?) groups related buttons, and ToolbarSeparator draws a divider.

SourceList

The sidebar’s list of places, like Finder’s or Mail’s. The list is a single Tab stop; ↑ and ↓ move between rows, and Return or Space opens one.
SourceList: label (required, for screen readers), footer? (pinned under the list). SourceListSection: title?, collapsible? (a disclosure on the title, as in Finder), defaultCollapsed?, action? (shown on hover at the title’s end, such as a ”+”). SourceListItem: label, onSelect (required), icon? or leading? (an avatar or cover), selected?, count?, shortcut? (shown on hover), disabled?. For a right-click menu, wrap it in a ContextMenu.

SelectionList

A list (or grid) with desktop selection: click selects, ⌘ / Ctrl-click and Shift-click extend it, ⌘A selects all, double-click or Return opens. Typing a few letters jumps to a matching row.
T[]
required
The rows’ data.
(item) => string
required
A stable key per item.
string[]
required
The selected keys.
(keys: string[]) => void
required
The selection changed.
(item, { selected }) => ReactNode
required
Draws a row or tile.
string
required
What the list is, for screen readers.
boolean
Allow more than one selected item.
(item) => void
Double-click, or Return on the selection.
(item, selection) => ContextMenuItem[]
Right-click menu. A row outside the selection is selected first, so the menu always acts on what’s highlighted.
(item) => string
Text for type-to-jump.
ReactNode
Shown when there are no items.
number
Row height in px. Defaults to the platform’s: 28 on macOS, 32 on Windows.
'list' | 'grid'
default:"'list'"
grid lays items out as tiles, like a photo library: ← and → step through tiles, ↑ and ↓ move a row.
number
default:"160"
The narrowest a tile gets in grid, in px.

Inspector

The pieces of an inspector pane, shaped like Finder’s Get Info:

useDesktopCommands

Declare the window’s commands once. On macOS they appear in your app’s menu in the system menu bar, where the shell turns their keys into menu clicks. Everywhere else (the Windows shell, a browser) they are bound as keyboard shortcuts in the page.
Each command has id, label, run, and optionally shortcut, enabled (default true; a disabled command’s key does nothing) and checked. A key without a modifier, such as delete, is always bound in the page, because the menu bar never carries one. A toolbar button for the same command should pass bindShortcut={false} so the key isn’t handled twice. This builds on the SDK’s useAppMenu.

Drag and drop

Mouse and trackpad drag and drop, desktop style. None of it gets in the way of touch; phones use swipe rows and context menus for the same jobs. Files from the desktop: wrap the target in DropZone. While files hover, it gets data-dropping and shows hint.
DropZone takes onFiles (required), accept? (MIME patterns, as in <input accept>), single?, disabled?, hint?, className? and style?. Your own items: make an element draggable with useDragSource(kind, data) and a target with useDropTarget(kind, onDrop, { canDrop? }). Spread the result on the element. While something droppable hovers, the target has data-drop-over.