react-os-shell
v0.2.38
Published
Desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted glass theming, and 17 bundled apps including a PDF Preview viewer.
Maintainers
Readme
react-os-shell
A desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted-glass theming — plus 16 bundled apps (utilities, games, Google integrations).
Status: v0.1.0 — extracted from a production ERP where it's been running for a small team. Public API is stabilising; expect minor shape changes before 1.0.
→ Live demo
A backend-less playground hosted on GitHub Pages. Wallpapers, themes, sticky notes, the spreadsheet, the calendar, all wired to localStorage so the page survives a refresh. Source is in examples/demo/.
The screenshot is auto-captured against the deployed demo by .github/workflows/screenshot.yml. Run it manually: gh workflow run "Capture hero screenshot" (or use the Actions tab).
What's in the box
Shell: <Layout>, <StartMenu>, <Desktop> (with sticky notes + folders), <WindowManager>, <Modal> (standard / compact / widget styles), <PopupMenu>, <ConfirmDialog>, <GlobalSearch> (Cmd-K), <ShortcutHelp>, <NotificationBell>, <BugReportDetail>, <StatusBadge>, frosted-glass theming, <GoogleConnectModal>.
Apps (16 ship in the package):
- Utilities (7): Calculator, Notepad, Spreadsheet, Weather, CurrencyConverter, PomodoroTimer, WorldClock
- Games (6): Chess, Checkers, Minesweeper, Sudoku, Tetris, 2048
- Google (3): Calendar, Email (Gmail), GeminiChat
15 of the 16 ship in the bundledApps registry today; the remaining one (WorldClock) is exported individually but needs consumer-supplied prefs wiring before slotting into bundledApps. The bundled Customization settings page is also exported separately for consumers to register at /settings/customization.
Hooks: useWindowManager, useTheme, full hotkey/nav system (useNewHotkey, useEditHotkey, useModalNav, useModalSave, useModalDuplicate, useTableNav, useMultiModal), useGoogleAuth, useEmailUnread.
Themes: light + dark (frosted-glass tinting; the package ships base styles, additional theme variants like pink/green/grey/blue can layer on top).
Install
npm i react-os-shellPeer deps you should already have in a typical React + Tailwind v4 app:
npm i react react-dom react-router-dom @tanstack/react-query react-hook-form \
tailwindcss @headlessui/react @heroicons/reactQuick start (~50 lines)
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
Layout,
WindowManagerProvider,
ConfirmProvider,
ShellAuthProvider,
ShellPrefsProvider,
ShellEntityFetcherProvider,
StatusBadgeProvider,
setShellApiClient,
setShellAuthBridge,
setShellWindowRegistry,
createWindowRegistry,
useLocalStoragePrefs,
} from 'react-os-shell';
import { bundledApps } from 'react-os-shell/apps';
import 'react-os-shell/styles.css';
import axios from 'axios';
const apiClient = axios.create({ baseURL: '/api' });
setShellApiClient(apiClient);
setShellWindowRegistry(createWindowRegistry(bundledApps));
setShellAuthBridge({ user: { first_name: 'Demo' }, logout: () => {} });
const navSections = [
{ to: '/', label: 'Home' },
{ label: 'Games', items: bundledApps['/chess'] ? [
{ to: '/chess', label: 'Chess' },
{ to: '/tetris', label: 'Tetris' },
{ to: '/2048', label: '2048' },
] : [] },
];
const queryClient = new QueryClient();
export default function App() {
const prefs = useLocalStoragePrefs('my-app');
return (
<QueryClientProvider client={queryClient}>
<ConfirmProvider>
<BrowserRouter>
<ShellAuthProvider value={{ hasAnyPerm: () => true }}>
<ShellPrefsProvider value={prefs}>
<ShellEntityFetcherProvider value={(endpoint, id) => apiClient.get(`${endpoint}${id}/`).then(r => r.data)}>
<StatusBadgeProvider groups={{}}>
<WindowManagerProvider>
<Routes>
<Route path="*" element={<Layout navSections={navSections} navIcons={{}} />} />
</Routes>
</WindowManagerProvider>
</StatusBadgeProvider>
</ShellEntityFetcherProvider>
</ShellPrefsProvider>
</ShellAuthProvider>
</BrowserRouter>
</ConfirmProvider>
</QueryClientProvider>
);
}That gives you the full desktop with all 12 utility/game/Google apps reachable through the start menu's Games / Utilities trays. Add your own entity windows by extending the registry, and wire the notification / bug-report / sticky-note systems through optional config callbacks when you want them.
Concepts
Window registry
Every window the shell can open lives in a WindowRegistry map. Two entry shapes:
- Page —
{ component: LazyExoticComponent, label, size?, widget?, … }. Opened viaopenPage(routeKey). - Entity —
{ endpoint, render(entity, …), title(entity), footer?, … }. Opened viaopenEntity(typeKey, id). The shell GETs${endpoint}${id}/(via the consumer-supplied entity fetcher) and hands the result torender.
Compose multiple partial maps with createWindowRegistry(...maps):
import { bundledApps } from 'react-os-shell/apps';
import { erpEntities } from './shell-config/erpEntities';
const windows = createWindowRegistry(bundledApps, erpEntities);
setShellWindowRegistry(windows);Nav sections
Layout renders the start menu from a (NavSection | NavItem)[] you pass in:
const navSections = [
{ to: '/', label: 'Home' },
{ label: 'Clients', items: [
{ to: '/orders', label: 'Sales Orders', perms: ['view_order'] },
{ to: '/clients', label: 'Clients' },
]},
];Items with perms are filtered through <ShellAuthProvider value={{ hasAnyPerm }}>.
useWindowManager
The hook every component uses to open / close / minimise windows:
const { openPage, openEntity, closeEntity, openWindows } = useWindowManager();
openPage('/calculator');
openEntity('order', 'uuid-123');API reference
All exports are named — import { Modal, ... } from 'react-os-shell'.
Components
| Export | Purpose |
|---|---|
| Layout | Top-level shell — desktop + taskbar + start menu. Mount once inside your providers. |
| StartMenu / Desktop / WindowManagerProvider | Used internally by Layout; rarely instantiated directly. |
| Modal, ModalActions, CopyButton, CancelButton | Window primitive supporting standard / compact / widget styles. |
| PopupMenu, PopupMenuItem, PopupMenuDivider, PopupMenuLabel | Right-click / context-menu primitive. |
| ConfirmProvider, confirm | Imperative confirm({ title, body }) returning a Promise. |
| GlobalSearch | Cmd-K command palette. Pass providers: SearchProvider[] to add results. |
| ShortcutHelp | The keyboard cheatsheet shown on ?. |
| NotificationBell | Taskbar bell — config via <Layout notifications={…}>. |
| BugReportDetail | Used inside an entity-window registry entry; reads from <BugReportConfigProvider>. |
| StatusBadge | Coloured pill rendering a status string. Map status→semantic group via <StatusBadgeProvider groups={{...}}>. |
| GoogleConnectModal | UI for entering Google OAuth client ID. |
Providers + setters
| Export | Use |
|---|---|
| <ShellAuthProvider value={{ hasAnyPerm }}> | Permission-filter nav items. |
| <ShellPrefsProvider value={{ prefs, save }}> | Where the shell reads/writes user prefs (theme, taskbar pos, sticky notes, …). Use useLocalStoragePrefs(key) for a backend-less default. |
| <ShellEntityFetcherProvider value={(endpoint, id) => …}> | How the modal stack fetches entity data. |
| <BugReportConfigProvider value={{ submit, list?, resolve? }}> | Wire the bug-report flow to your backend. |
| <DesktopHostProvider value={{ stickyResolver?, saveShortcuts?, … }}> | Sticky-note ref resolver + persistence callbacks. |
| <StatusBadgeProvider groups={{ status: 'success' \| ... }}> | Status string → semantic group. |
| setShellApiClient(axios) | Module-level: register your axios instance once. |
| setShellAuthBridge({ user, logout }) | Module-level: register user identity / logout handler. |
| setShellWindowRegistry(registry) | Module-level: register your composed WindowRegistry. |
Hooks
| Export | Purpose |
|---|---|
| useWindowManager() | { openPage, openEntity, closeEntity, openWindows, … } |
| useTheme() | { theme, resolved } — current theme + system-resolved value. |
| useNewHotkey(handler) | Cmd/Ctrl+N — for "create new entity" buttons. |
| useEditHotkey(handler) | Alt+Shift+E — for "edit" toggle. |
| useModalNav({ onPrev, onNext }) | ←/→ to step through siblings inside a modal. |
| useModalSave(handler) | Cmd-S inside a modal. |
| useModalDuplicate(handler) | Alt-D inside a modal. |
| useTableNav({ rows, cols, onCell }) | Arrow-key cell navigation in editable grids. |
| useMultiModal() | Manages multi-window stacking + activate/blur. |
| useGoogleAuth({ clientId? }) | Google Identity Services wrapper — token + scopes. |
| useEmailUnread() | Live unread-count for the Gmail badge. |
| useShellAuth() / useShellPrefs() / useShellEntityFetcher() / useBugReport() / useDesktopHost() | Context readers — the shell uses these internally; consumers may also call them. |
Apps barrel — react-os-shell/apps
| Export | Type |
|---|---|
| bundledApps | WindowRegistry — 12 ready-to-mount apps. |
| utilityApps, gameApps, googleApps | Subsets of bundledApps. |
| Calculator, Spreadsheet, Weather, CurrencyConverter, PomodoroTimer, Chess, Checkers, Sudoku, Tetris, Game2048, Email, GeminiChat | Lazy components — use directly in custom registry entries. |
Misc
| Export | Notes |
|---|---|
| createWindowRegistry(...maps) | Variadic merge — later partials override earlier on the same key. |
| isPageEntry, isEntityEntry | Type guards for WindowRegistryEntry. |
| glassStyle() | Returns the theme-aware frosted-glass style object. |
| reportBug(submit) | Captures a screenshot via getDisplayMedia, opens the dialog, hands the payload to your submit. |
| formatDate(iso) | Locale-aware date formatter. |
| toast.success / .error / .info | Toast notifications — auto-mounts container. |
| Kbd constants — MOD, ALT, SHIFT, ENTER, ALT_SHIFT_E, CMD_K, … | Symbol constants for rendering keyboard shortcuts. |
Why it exists
Most "desktop UI" demos on the web are toys with hardcoded windows and no escape hatch. This one was extracted from a working ERP where every entity (sales orders, invoices, vendors, …) opens as its own window with consistent header, footer, hotkeys, depth stacking, and split-view. The shell is fully decoupled from any specific backend — every subsystem that needs server data (notifications, bug reports, desktop shortcuts, search, entity fetching) takes its data through callback configs supplied by the consumer. Drop-in localStorage fallbacks ship for prefs and sticky notes so the package works out of the box without a backend.
Examples
examples/demo— small Vite app showcasing the shell + bundled apps with mock data. Live at victorymau.github.io/react-os-shell, deployed automatically by.github/workflows/pages.ymlon every push tomain.
Contributing
PRs welcome. Open an issue first for non-trivial changes so we can align on shape.

