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.

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.