Skip to content

Shell surfaces

A shell surface is one accessibility root owned by the operating system rather than by an application. Menu bars, status items, taskbars, panels, docks, the desktop, and open flyouts are all shell surfaces. The Kinds table below gives each one per platform.

ShellSurface is the entry point for those roots, as App is for application roots. Each surface wraps an element the platform itself vends, tagged with a kind. Selectors, locators, auto-waiting, actions, tree(), dump(), and every error type work on a shell surface exactly as they do on an application.

The kind crosses every binding as its snake_case string, with the same spelling in Rust, Python, JavaScript, the CLI, and MCP.

Kind Windows macOS Linux
menu_bar (none) The active application’s AXMenuBar, Apple menu included. Name and process id are the owning application’s. (none)
status_items (none) One surface per process with a live AXExtrasMenuBar. Name and process id are the owning application’s. (none)
taskbar The Shell_TrayWnd pane, holding the task band and the visible tray row with its overflow chevron. (none) (none)
panel (none) (none) Each AT-SPI frame carrying window-type:dock. One surface per frame, so a panel and a dock row are two surfaces.
dock (none) The Dock process’s application element. (none)
desktop The SysListView32 desktop list view, reached as ProgmanSHELLDLL_DefViewSysListView32. A shell without that chain contributes no desktop surface. Finder’s desktop scroll area. (none)
flyout The tray overflow, Quick Settings, and shell context-menu popups, while open. (none), not in v1. (none)
unknown (none) (none) (none)

A cell reading (none) means the platform vends no such surface. list() on Linux returns no taskbar, and by_kind for a kind that platform lacks fails with SelectorNotMatched naming what was found instead. Windows has no per-process tray hosting, so tray icons live inside the taskbar and flyout surfaces rather than under status_items.

Two of the empty cells are a scoping decision rather than a platform gap. macOS emits no flyout in v1: an opened Control Center or Notification Center panel is a shell process’s AXSystemDialog window, whose enumeration contract differs from every other macOS surface. Windows leaves the Notification Center out of its flyout set, because its window class is the one ordinary UWP apps also use and separating them takes a process-image lookup the backend does not perform. An absent surface is honest; a misclassified application window is not.

The unknown kind is a reserved fallback, so a shell window some future backend cannot classify degrades to reachable-but-untagged instead of vanishing from the listing. No backend emits it in v1, so by_kind("unknown") matches nothing today.

The kind set grows as more shell UI is classified, so handle an unknown kind string the way you handle an unknown role.

Member Meaning
kind The kind string from the table above
name The owning application for per-application surfaces, the platform’s own name otherwise (Taskbar, Dock)
pid Owning process, where the platform reports one. Optional.
locator(selector) A Locator rooted at the surface, with the usual auto-wait and action set
children() The surface root’s direct children
tree(max_depth) A structured snapshot, same shape as App.tree()
dump(max_depth) The same snapshot as indented text
as_element() The surface root as an ordinary Element

pid reports the process the platform attributes the surface to, which is not always the process that drew the icon you are looking at. On macOS it is the true owner. On Windows it is the host process (explorer.exe), because UIA carries no per-icon owner. On Linux it is the panel process.

The surface root also carries its kind in its raw platform attributes, under the key shell_kind, so it shows up in tree() and dump() output and reads back as surface.as_element().raw["shell_kind"]. That is a read, not a selector hook: a locator rooted at the surface matches the surface’s descendants, never the root itself, so surface.locator("[shell_kind='taskbar']") does not match the surface it is rooted at.

ShellSurface::list() and ShellSurface::by_kind() come from the ShellSurfaceExt trait, mirroring AppExt.

use std::time::Duration;
use xa11y::{ShellSurface, ShellSurfaceExt, ShellSurfaceKind};
for surface in ShellSurface::list()? {
println!("{:?}\t{:?}\t{}", surface.kind, surface.pid, surface.name);
}
// `Duration::ZERO` makes one attempt; anything longer polls.
let bar = ShellSurface::by_kind(ShellSurfaceKind::MenuBar, Duration::from_secs(2))?;
bar.locator("menu_item[name='Save']").press()?;

by_kind requires exactly one surface of that kind. Several status_items processes, several panel frames, or a second flyout produce SelectorNotMatched with a diagnosis listing every candidate by kind, name, and process id. Call list() and pick by process id to disambiguate.

Process id is the only lever, and it does not always separate the candidates. One Linux panel process can draw two panel frames, which are then two surfaces sharing one pid, and no pid picks between them. A refusal in that state says so in its diagnosis rather than offering a filter that cannot work. list() still returns both surfaces, so a caller that walks the list can pick on whatever else tells them apart, such as the root element’s bounds.

Listing is live, and it reads only. Enumerating surfaces and dumping their trees never opens, closes, focuses, or presses anything. So a flyout surface appears in list() while it is on screen and disappears from the next call once it closes.

Content that exists only while a flyout is open is reached by pressing a real element and then asking for the surface again. Windows tray icons in the overflow are the case that comes up most: they are absent from the tree until the chevron opens the flyout that hosts them.

taskbar = xa11y.ShellSurface.by_kind("taskbar", timeout=2.0)
taskbar.locator("button[name='Show Hidden Icons']").press()
flyout = xa11y.ShellSurface.by_kind("flyout", timeout=3.0)
flyout.locator("button[name*='Tailscale']").press()

The press is an ordinary accessibility action on an element the platform advertises. No shell API opens a surface on the caller’s behalf.

xa11y shell lists surfaces, and --shell KIND targets one from tree, find, and action. It is mutually exclusive with --app, and --pid alongside it picks between same-kind surfaces. See CLI.

Terminal window
xa11y shell
xa11y tree --shell taskbar
xa11y find "button[name*='Tailscale']" --shell flyout
xa11y action press "button[name='Show Hidden Icons']" --shell taskbar

The shell tool lists surfaces as {kind, name, pid} rows. The tree, find, and action tools take an optional shell argument holding a kind string, mutually exclusive with app and combinable with pid. A kind that matches several surfaces returns an ambiguous_shell_surface failure carrying the candidate list. See CLI for the argument tables and Serve xa11y over MCP for client setup.