Getting started
Install the package, wrap your app in a provider, drop a viewport somewhere, and contribute from anywhere in the tree. React 18+ is the only peer dependency.
Install
Add the package with your favourite package manager:
$ bun add @mrmartineau/react-status-barreact and react-dom (18 or newer) must already be installed in your app. Nothing else is bundled.
Mount the provider + a viewport
The provider holds the store. The viewport reads a scope and renders it — give the shell node a min-height so content arriving after hydration causes no layout shift.
// AppShell.tsx
import { StatusBarProvider, StatusBarViewport } from "@mrmartineau/react-status-bar";
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<StatusBarProvider>
<div className="app-layout">
{/* A fixed shell node in your chrome */}
<div id="statusbar-global" className="statusbar-shell" />
<StatusBarViewport
portalTarget="#statusbar-global"
mode="stack"
separator="•"
/>
{children}
</div>
</StatusBarProvider>
);
} Contribute from anywhere
Mount a <StatusBar> anywhere below the provider. It declares what to show and how important it is — the viewport decides presentation. Entries are removed automatically on unmount.
import { StatusBar } from "@mrmartineau/react-status-bar";
function Editor() {
return (
<>
<StatusBar priority={3}>Autosaving…</StatusBar>
<StatusBar priority={1}>Preview mode</StatusBar>
</>
);
} Imperative updates (optional)
For event handlers and async flows, useStatusBar() returns show() / hide(). show is an idempotent upsert — call it again to update the same entry in place.
import { useStatusBar } from "@mrmartineau/react-status-bar";
function SaveButton() {
const sb = useStatusBar();
async function onClick() {
sb.show("Saving…", { priority: 5 });
try {
await save();
sb.show("Saved", { priority: 3 }); // same entry, updated
setTimeout(() => sb.hide(), 1500);
} catch {
sb.show("⚠️ Save failed", { priority: 0 }); // escalate to P0
}
}
return <button onClick={onClick}>Save</button>;
} Multiple bars with scopes
Every component takes a scope. One provider can drive any number of independent bars; a viewport only re-renders when its own scope changes.
<StatusBarViewport scope="global" portalTarget="#statusbar-global" />
<StatusBarViewport scope="editor" portalTarget="#statusbar-editor" mode="stack" />
// deep in the editor tree
<StatusBar scope="editor">Spellcheck enabled</StatusBar> The package is marked "use client". Producers and viewports are client components (the store lives in the browser), but they can be rendered anywhere inside a server-rendered tree. The server snapshot is empty, so there is never a hydration mismatch.
Inject a store for isolation: <StatusBarProvider store={createStatusStore()}>. The store is plain JS — unit-test upsert/remove/sorting without React at all.