Skip to content

Launcher

The launcher is the keyboard front door: type a few letters, get the right window. Every OS presents this differently, so the library splits it into a shared brain and a swappable face. The data (apps, system windows, and registerSpotlightSource rows), the Cmd-K shortcut, selection, and activation are the same in every register; theme.chrome.launcher picks the layout.

The launcher in its Spotlight register Open in new tab ↗

theme.chrome.launcher chooses the face. All three carry the same results and filter the same way.

ValueReads asLayout
"spotlight" (default)macOS SpotlightA centered command palette: search field over a vertical result list.
"grid"GNOME ActivitiesA full-bleed overview: a centered search field over a grid of large app icons.
"menu"Windows StartA panel raised from the dock launcher: a search field over a grid of app tiles.

The default theme uses "spotlight", Ubuntu uses "grid", and Windows uses "menu". The arrow keys navigate every register (the grid and menu add left/right); Enter activates; Escape dismisses and restores focus.

The "menu" register is built on the 25H2 redesigned Start: one vertically scrollable page with Pinned at the top (two rows until “Show all”, remembered across opens), the Recent region in the middle, and the All apps section at the bottom. All apps renders in three switchable, persisted views: Category (the default, folder cards that open a flyout of the category’s apps), Grid (alphabetical icon groups with a click-to-jump letter picker), and List (compact rows). Apps group by the optional App.category; a category forms only with at least three apps, the rest fold into Other.

It carries the Windows Start personalization controls (Insider builds, May 2026) as chrome tokens: startMenuSize is "auto" (large on a wide viewport, small on a narrow one), "small" (6-column), or "large" (8-column); startMenuPinned, startMenuRecent, and startMenuAllApps show or hide each section independently; startMenuRecentFiles turns off file rows in Recent without hiding recently used apps; startMenuProfile hides the name and profile picture in the footer. The Recent section lists recently used apps plus the files contributed via registerRecentsSource. The Windows theme exposes all of these in its Settings Start section.

import { Launcher, useLauncher, SPOTLIGHT_OPEN_EVENT } from "@react-ui-os/desktop";

<Launcher> is mounted by the default <Desktop>. <Spotlight> is still exported as a back-compat alias of <Launcher>.

TriggerBehavior
Cmd-K (macOS) / Ctrl-K (elsewhere)Toggles it. Bails when typing in an input or textarea.
window.dispatchEvent(new CustomEvent(SPOTLIGHT_OPEN_EVENT))Opens from any component without prop drilling.
The dock launcher buttonDispatches the event above. The bar dock shows it; the menu and grid pop where the OS does.
KindSource
AppEvery entry in the apps prop passed to <Desktop> / <DesktopProvider>.
SystemEvery entry in the systemWindows registry (Settings by default, plus any you register).
ExternalEvery result returned by a function registered via registerSpotlightSource.

Activating an App or System result calls openWindow(...), the same primitive a dock click uses. External results run their own onActivate so they can navigate, dispatch, or open a URL.

Use registerSystemWindow(...) when activating the result should open a window managed by react-ui-os. Settings is the canonical example.

Use registerSpotlightSource(...) when activating should do something else: navigate to a docs page, open a URL, dispatch an event. A source is (query) => SpotlightResult[], and each result owns its onActivate. Sources feed every presentation, not just Spotlight.

import { registerSpotlightSource } from "@react-ui-os/desktop";
useEffect(() => {
return registerSpotlightSource("bookmarks", (query) => {
if (!query) return [];
return bookmarks
.filter((b) => b.name.toLowerCase().includes(query))
.map((b) => ({
id: b.id,
name: b.name,
tagline: b.url,
kindLabel: "Bookmark",
onActivate: () => window.open(b.url, "_blank", "noopener"),
}));
});
}, []);

When the three presentations do not fit, build your own face on the same brain. useLauncher() returns the open state, the query, the merged and filtered results, the selection, and activate / moveSelection / close. It also owns the Cmd-K toggle and the open event, so a custom launcher gets those for free. Render it at depth 2 in place of <Launcher>.

import { useLauncher } from "@react-ui-os/desktop";
function MyLauncher() {
const { open, query, setQuery, results, activate, close } = useLauncher();
if (!open) return null;
return (
<div role="dialog" aria-label="Launcher">
<input value={query} onChange={(e) => setQuery(e.target.value)} autoFocus />
{results.map((r) => (
<button key={r.key} onClick={() => activate(r)}>
{r.name}
</button>
))}
<button onClick={close}>Close</button>
</div>
);
}
  • The search field is a role="combobox" wired to its results listbox with aria-activedescendant, so screen readers announce the highlighted result as the arrows move it.
  • The Spotlight backdrop is role="presentation" with click-to-dismiss; the panels are role="dialog".
  • Each result is a role="option" with aria-selected.
  • Focus restores on close and is never trapped.