Gecko UIGecko UI

GeckoUIProvider

Context-aware provider that hosts the overlay stack and toast system

GeckoUIProvider

GeckoUIProvider wraps your application tree and is required for Dialog.show(), Drawer.show(), ConfirmDialog.show(), and toast. It owns a shared overlay stack rendered via ReactDOM.createPortal, which means:

  • Overlay content opened with the imperative API can read any React context provided above GeckoUIProvider.
  • Multiple dialogs and drawers can be open simultaneously and stack correctly.
  • Dismissing the topmost overlay reveals the one beneath.

Setup

// app/layout.tsx
import { GeckoUIProvider } from "@geckoui/geckoui";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <GeckoUIProvider>
          {children}
        </GeckoUIProvider>
      </body>
    </html>
  );
}

Context ordering: Place GeckoUIProvider below your own context providers so overlays can read those contexts.

<AuthProvider>
  <ThemeProvider>
    <GeckoUIProvider>   {/* ← overlays can now read AuthContext and ThemeContext */}
      <App />
    </GeckoUIProvider>
  </ThemeProvider>
</AuthProvider>

Nesting providers

Mount one provider at the root. If you nest another one deeper — for example on a page that has its own context you want overlays to read — the innermost provider owns the overlay stack and the <Toaster>, and the outer ones just render their children. A warning is logged so accidental duplicates are easy to spot.

<GeckoUIProvider>            {/* root: renders nothing while the inner one is mounted */}
  <PageUserContext.Provider value={user}>
    <GeckoUIProvider>        {/* ← owns the stack, so overlays can read PageUserContext */}
      <Page />
    </GeckoUIProvider>
  </PageUserContext.Provider>
</GeckoUIProvider>

Props

PropTypeDefaultDescription
childrenReactNode—Your application tree
toastOptionsToasterProps{}Options forwarded to sonner's <Toaster>

Toast customisation

<GeckoUIProvider toastOptions={{ duration: 3000, position: "top-center" }}>
  <App />
</GeckoUIProvider>

Stacking overlays

Each Dialog.show() / Drawer.show() call pushes a new entry and returns an id:

const firstId = Dialog.show({
  content: ({ dismiss }) => (
    <div>
      <p>First dialog</p>
      <Button onClick={() =>
        Dialog.show({ content: () => <p>Second dialog (on top)</p> })
      }>
        Stack another
      </Button>
      <Button onClick={dismiss}>Close this</Button>
    </div>
  )
});

// Close a specific dialog:
Dialog.dismiss(firstId);

// Close the topmost dialog:
Dialog.dismiss();

Dialog.dismiss() and Drawer.dismiss() only close overlays of their own type. Calling Dialog.dismiss() never closes a drawer, and the other way round.

Stacking order

  • Drawers render at z-index: 1000 + position in the stack.
  • Dialogs render at z-index: 2000 + position.

So a dialog always sits above a drawer, whichever one opened last, and the dialog is the one that captures Esc and click-outside. An overlay gives up topmost as soon as it starts closing, so the one underneath takes over right away instead of waiting for the 300ms exit animation.

Missing provider

Dialog.show(), Drawer.show(), and ConfirmDialog.show() need a mounted provider. If none is mounted, nothing renders and an error is logged:

[GeckoUI] No <GeckoUIProvider> is mounted, so the overlay cannot be rendered.
Wrap your app with <GeckoUIProvider> to use Dialog.show() and Drawer.show().

The declarative <Dialog> and <Drawer> components render in place and work without the provider.

Context consumption

// AuthContext is provided above GeckoUIProvider
const { user } = useContext(AuthContext);

Dialog.show({
  content: () => {
    // This component can call useContext(AuthContext) and get the real value
    const { user } = useContext(AuthContext);
    return <p>Hello, {user.name}</p>;
  }
});

Limitations

Overlays lock page scroll while open, sharing one reference counted lock, and compensate for the scrollbar so the page does not shift. The useScrollLock(enabled) hook is exported if you need it for your own overlays.

Still missing: overlays have no focus trap and do not restore focus to the trigger on close. iOS Safari ignores overflow: hidden on the body, so the page behind can still be dragged there.

Migration from GeckoUIPortal

Replace the self-closing <GeckoUIPortal /> with the <GeckoUIProvider> wrapper:

- import { GeckoUIPortal } from "@geckoui/geckoui";
+ import { GeckoUIProvider } from "@geckoui/geckoui";

  export default function RootLayout({ children }) {
    return (
      <html>
        <body>
-         {children}
-         <GeckoUIPortal toastOptions={{ duration: 3000 }} />
+         <GeckoUIProvider toastOptions={{ duration: 3000 }}>
+           {children}
+         </GeckoUIProvider>
        </body>
      </html>
    );
  }
  • Dialog — Imperative modal dialogs
  • Drawer — Imperative slide-out panels
  • ConfirmDialog — Pre-styled confirmation dialogs