Library / SDK
toss/overlay-kit avatar
toss/overlay-kit

overlay-kit, declarative overlays for React without a state library

A library for handling overlays more easily in React.

737 stars81 forksTypeScriptMIT

At a glance

What is it?
toss/overlay-kit is a small React library from the Toss team that manages modals, popups and dialogs as overlays you open imperatively and control declaratively, with an async variant that returns a promise so a dialog can return a value. It works across React 16 to 19 and has no runtime dependencies.
Who is it for?
Adopt overlay-kit if your React application has more than a couple of modals and the state for them has started leaking into your component tree, and you want an overlay to return a value without hand-rolled callback plumbing. Skip it if you need a design system with a full dialog suite, because this is a control mechanism rather than a component library.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 22 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library does and the problem it names

overlay-kit is a library for declaratively managing overlays like modals, popups and dialogs in React. The stated benefit is that you can implement overlays efficiently without complex state management or unnecessary event handling.

The README is unusually clear about the problem it is solving, and it names three parts. The first is the complexity of state management. You had to manage overlay state directly with useState or global state, and the code became complex and less readable as that state management got mixed with UI logic. The second is repetitive event handling, where the code to open, close and return a result from an overlay was written again and again, leading to duplication. The third is a lack of reusability, because UI and logic were tightly coupled through the callback functions used to return values from an overlay, which made components hard to reuse.

Those are specific complaints, and they are the ones that come up in any application with a meaningful number of modals. A confirmation dialog, a form sheet, a side panel and a toast are all overlays, and each one typically ends up needing an isOpen boolean somewhere, a close handler, and a way to get a result back to whoever opened it. The result plumbing is the part that hurts, because a callback passed down through props is a callback that has to be threaded through every component in between.

The response is a small set of goals. Design following React philosophy, on the grounds that React favours declarative code. Improve development productivity by encapsulating the state management and the event handling so developers can focus on UI and business logic. And enhance extensibility and reusability by separating UI from behaviour and returning promises.

The licence is MIT, held by Viva Republica, Inc., which is the company behind Toss, and the README carries the Toss logo and a link to their site. The documentation site is overlay-kit.slash.page.

Getting started and the two open methods

Installation is a single npm command:

sh
npm install overlay-kit

There is one setup step, which is adding the provider at the root of the application:

tsx
import { OverlayProvider } from 'overlay-kit';

const app = createRoot(document.getElementById('root')!);
app.render(
  <OverlayProvider>
    <App />
  </OverlayProvider>
);

That is the whole of it. The provider exists because the library needs a place to hold the overlay state and to render the overlays that are open, and putting it at the root is the only requirement.

The first method is overlay.open, which takes a render function and returns controls. The example shows a button whose click handler calls overlay.open with a function receiving isOpen, close and unmount:

tsx
import { overlay } from 'overlay-kit';

<Button
  onClick={() => {
    overlay.open(({ isOpen, close, unmount }) => (
      <Dialog open={isOpen} onClose={close} onExit={unmount} />
    ))
  }}
>
  Open
</Button>

The three controls have distinct jobs and the distinction matters. isOpen is the open state, passed to your dialog component so it can show itself. close is what you call when the user dismisses it, and it takes whatever value you want to hand back. unmount is the cleanup hook, and the example wires it to the dialog's onExit prop, which is how the overlay is torn down once the exit animation has finished.

The second method is overlay.openAsync, which is the one that solves the result plumbing. It returns a promise:

tsx
import { overlay } from 'overlay-kit';

<Button
  onClick={async () => {
    const result = await overlay.openAsync<boolean>(({ isOpen, close, unmount }) => (
      <Dialog
        open={isOpen}
        onConfirm={() => close(true)}
        onClose={() => close(false)}
        onExit={unmount}
      />
    ))
  }}
>
  Open
</Button>

The generic parameter names the result type, here boolean, and the two close calls carry the two possible outcomes. The caller awaits the promise and gets a value, so the code that opened the dialog continues in the same place rather than in a callback defined several components away.

That is the whole API surface described in the README, and it is worth noticing how little it is. There is a provider, a namespace with two methods, and three controls handed to your render function.

The repository layout and the tooling

The repository is a Yarn workspace monorepo and the root package.json shows how it is put together.

The workspaces field covers the root itself, a packages directory, a docs directory, a docs-old directory and the examples glob. The presence of a docs-old directory is a small signal: the documentation has been reorganised and the previous tree is kept, which is common for a project that moved its site infrastructure and wanted to keep the old pages reachable.

The scripts are minimal. Lint runs eslint across the repository. The two release scripts run the changesets tooling, changeset:version and changeset:publish, which is the standard Changesets workflow for versioning and publishing packages. There is no build script at the root, which is what you would expect from a library consumed as source or built per package rather than from a single root build.

The dependency story is the notable part. The devDependencies are the linting and type checking toolchain: eslint with the flat config support package, the prettier integration, the import, unicorn and unused-imports plugins, prettier itself, typescript and typescript-eslint, and the changesets CLI with its GitHub changelog plugin. Notably, there is no React in the devDependencies at all, which tells you the library has no peer dependency requirement baked into the tooling, and that the examples directory carries the React dependency for each example.

The lint configuration is a flat config file, eslint.config.cjs, which matches the modern eslint 9 setup with the eslint package itself still on version 8. There is a .prettierrc and a .prettierignore, a .nvmrc pinning the Node version, a .vscode directory, and a .yarn directory with a .yarnrc.yml, which is the Yarn 4 setup with the releases tracked in the repository.

The examples directory is the part that tells you about compatibility. There are four of them, for React 16, React 17, React 18 and React 19, one per major version. Having a working example per React major is how a library like this proves it works across the range, and the fact that all four are checked in rather than just claimed is the useful part.

The docs directory holds the documentation site, and the repository has a codecov.yml, so coverage is tracked in continuous integration. There is also a .changeset directory, which is where the pending version bumps live before a release.

Where it fits against the alternatives

The alternatives split into two kinds, and it is worth being clear about which problem each one solves.

The first is a component library with dialogs in it. Radix UI, Headless UI, React Aria and the various design system kits all ship dialog, popover and dropdown components. What they do not ship is the mechanism for opening one from anywhere in the tree and getting a result back. You still need a portal, some context, and a way to pass a value out, and that is the machinery overlay-kit replaces.

The second is a state manager. Redux, Zustand and Jotai can hold an isOpen flag for a dialog perfectly well, and many applications do exactly that. What they do not give you is a promise, so the result still has to come back through a callback stored somewhere, which is the specific complaint in the README about UI and logic being tightly coupled through callback functions.

The interesting middle case is React 19's own use hook, which is the closest thing to what overlay-kit provides. It gives you a promise for a modal, closes over a component rather than a function, and integrates with form actions. That is very close to overlay-kit's openAsync, and for a new application on React 19 it is the comparison you have to make. The differences are that use is a hook and therefore needs a component boundary to render the modal into, while overlay-kit takes a render function and mounts it, and that use is tied to a React version while overlay-kit works across React 16 to 19.

The case for a small dedicated library rather than either of those is about consistency. If the same application has forty overlays, and each is opened in a slightly different ad hoc way, the inconsistency is the real cost. A single namespace with two methods and a provider means every dialog in the codebase opens the same way, handles unmounting the same way, and returns its value the same way.

The costs are the usual ones for a small library. It is a dependency with its own release cycle, and it only handles overlays, so you still need actual dialog components from somewhere. The unmount control is also a responsibility the library hands to you: if you never call it, the overlay stays mounted, and the README's examples always wire it to an exit callback, which suggests the author expects you to think about it.

The judgement is that this is a well-scoped answer to a real problem, from a team whose primary business is a large React application and whose primary users are therefore the people who hit the problem first. The fact that it is small enough to read in an afternoon is a reasonable proxy for it being small enough to trust.

Editorial conclusion

Adopt overlay-kit if your React application has more than a couple of modals and the state for them has started leaking into your component tree, and you want an overlay to return a value without hand-rolled callback plumbing. Skip it if you need a design system with a full dialog suite, because this is a control mechanism rather than a component library. Verify first by installing it, adding the provider once at the root, and converting one dialog to openAsync to confirm the promise result path works with your React version.

Frequently asked questions

What does overlay-kit do?

It is a React library for declaratively managing overlays such as modals, popups and dialogs, so you can open an overlay from anywhere in the tree and get a result back without threading state and callbacks through the component hierarchy.

How do I set up overlay-kit?

Install it with npm install overlay-kit, then wrap your application in the OverlayProvider at the root, creating the root with createRoot and rendering the provider around your App component. That is the only setup step the README describes.

What is the difference between overlay.open and overlay.openAsync?

overlay.open takes a render function receiving isOpen, close and unmount, and opens the overlay. overlay.openAsync does the same but returns a promise, so the awaiting code receives the value passed to close, and the result type can be named with a generic parameter such as boolean.

What do isOpen, close and unmount do?

isOpen is the open state to pass to your dialog, close dismisses the overlay and takes the value you want to return to the caller, and unmount is the cleanup hook that tears the overlay down, which the examples wire to the dialog's onExit prop so the exit animation can finish first.

Which React versions does overlay-kit support?

The repository includes separate example applications for React 16, React 17, React 18 and React 19. The licence is MIT, held by Viva Republica, Inc., the company behind Toss, and the releases include overlay-kit 1.9.0 from February 2026.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. toss/overlay-kit on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/toss-overlay-kit.svg)](https://hysenlabs.com/projects/toss-overlay-kit)

Was this useful?