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.

1

Install

Add the package with your favourite package manager:

$ bun add @mrmartineau/react-status-bar
Peer dependencies

react and react-dom (18 or newer) must already be installed in your app. Nothing else is bundled.

2

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.

tsx
// 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>
  );
}
3

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.

tsx
import { StatusBar } from "@mrmartineau/react-status-bar";

function Editor() {
  return (
    <>
      <StatusBar priority={3}>Autosaving…</StatusBar>
      <StatusBar priority={1}>Preview mode</StatusBar>
    </>
  );
}
4

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.

tsx
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>;
}
5

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.

tsx
<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>
RSC / Next.js

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.

Testing

Inject a store for isolation: <StatusBarProvider store={createStatusStore()}>. The store is plain JS — unit-test upsert/remove/sorting without React at all.

idle — no global status