# nice-modal-react: giving every modal a global id

> nice-modal-react is eBay's zero-dependency modal state manager for React, about 2kb gzipped, that shows a dialog by string id and returns a promise when the task inside finishes. It renders nothing, delegates every pixel to your UI library, and its published release line has not moved since 1.2.13 in October 2023.

**eBay/nice-modal-react** — A modal state manager for React.

- Repository: https://github.com/eBay/nice-modal-react
- Website: https://ebay.github.io/nice-modal-react
- Stars: 2,298 · Forks: 126
- Language: TypeScript
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/ebay-nice-modal-react

## It renders no modal of its own

The README states the most important thing about this library in a note near the top: `@ebay/nice-modal-react` is not a React modal component, and it should be used with other modal or dialog implementations from UI libraries such as Material UI, Ant.Design or Bootstrap React.

That single sentence changes what you are installing. You get state management, not pixels. No portal, no overlay, no focus trap, no transition handling, no accessibility scaffolding. That belongs to whatever dialog component you already use, and this library sits beside it.

For that scope the size claim is credible rather than impressive. Zero dependencies, and roughly 2kb after gzip. The implementation is a React context that holds modal state globally so modals can be shown or hidden either from inside the modal component or by id.

The audience is a team that has standardised on a component library and cannot agree where modals should be declared. This is a state-placement problem dressed up as a modal problem, and if you do not have that problem this library solves something you do not have.

## The id is borrowed from routing

The README's motivation section does something rarer than listing features. It names a specific scenario, a dialog that creates a JIRA ticket and can be opened from the header, a context menu or a list page, then explains why the two conventional answers both fail.

Declaring the modal wherever it is used means managing its visibility and arguments at every call site. Declaring it in the root component means the root maintains state for every modal in the app, which does not scale, and it still leaves you passing `setVisible` down to every child that needs to open it.

Then the argument that drives the design. Quoting the Wikipedia definition of a modal window, a window that prevents the user from interacting with your application until he closes it, the README concludes a modal is a global view not necessarily tied to a specific context. A single-page application already solves the identical problem for pages with a router, where a URL acts as a global identifier.

So a modal gets the same treatment. Assign it a unique id, show and hide it by id, and the call site no longer has to know where the component lives. That is the entire idea, and everything else in the API is bookkeeping for it.

## Installing and wrapping a dialog

Installation is a normal dependency add, from either package manager.

```bash
npm install @ebay/nice-modal-react
```

The README uses an Ant.Design dialog in its worked example, which makes the division of labour obvious. `Modal` comes from `antd`. `NiceModal.create` is a higher-order component that supplies the state, and `useModal` is the hook the wrapped component reads.

```jsx
import { Modal } from 'antd';
import NiceModal, { useModal } from '@ebay/nice-modal-react';

export default NiceModal.create(({ name }: { name: string }) => {
  const modal = useModal();
  return (
    <Modal
      title="Hello Antd"
      onOk={() => modal.hide()}
      visible={modal.visible}
      onCancel={() => modal.hide()}
      afterClose={() => modal.remove()}
    >
      Hello {name}!
    </Modal>
  );
});
```

Three things in that snippet are the API's real shape. `modal.visible` is what you feed the dialog's visibility prop. `modal.hide()` is called from inside the modal, which is what makes it uncontrolled. And `afterClose={() => modal.remove()}` is the cleanup step, wiring your dialog's exit animation to unregistering the entry rather than to unmounting it.

That last point is why the key features list says it doesn't break the transitions when showing or hiding a modal. Remove and hide are separate operations, so the exit animation gets to finish.

## show() returns a promise, so a modal becomes a task

The call site is one line, and it returns a promise.

```jsx
import NiceModal from '@ebay/nice-modal-react';
import MyModal from './MyModal';

NiceModal.show(MyModal, { someProp: 'hello' }).then(() => {
  // do something when the task in the modal finishes.
});
```

The README's phrasing is precise and worth keeping in mind when reading the API: the promise fires when the task in the modal finishes, not when the modal opens and not when data is submitted. The component decides, by calling the relevant method, when the task is done.

This is the feature that separates it from a plain context. Most modal helpers give you a global `show`. This one gives you a promise, which means an `async` caller can await a dialog the same way it awaits a fetch, and a chain of modal interactions reads top to bottom rather than across a state machine.

The cost is that the resolution value is not specified in the README. There is no documented way to pass a result back out of the modal through that promise, so if your dialog needs to return a value to the caller you will be reaching for component state and props anyway. The promise is a completion signal, not a data channel, and the README does not claim otherwise.

## Register once, show from anywhere

The first example imports the modal component at the call site. The second removes that import, which is the decoupling the README advertises.

```jsx
import NiceModal from '@ebay/nice-modal-react';
import MyModal from './MyModal';

NiceModal.register('my-modal', MyModal);

NiceModal.show('my-modal', { someProp: 'hello' }).then(() => {
  // do something when the task in the modal finishes.
});
```

Registration is a module-level step. Once `register('my-modal', MyModal)` has run, a string is enough to open it, and the file that opens it does not have to know the component's path, its props interface, or whether it exists.

That is a genuine architectural choice rather than a convenience. It inverts the dependency so the trigger lives near the user action and the modal lives near the feature it belongs to. The cost is that the id becomes a stringly-typed contract across your codebase, with no compile-time check that the id exists or that the props match, and nothing in the README suggests TypeScript generics are used to close that gap.

You can also use both forms. Importing the component directly works for a modal used in exactly one place, and registering by id pays off for the JIRA-ticket case the README describes, where the same dialog is opened from three unrelated components.

## Uncontrolled, and not mounted until needed

Three of the key features describe lifecycle rather than API, and they are the ones with real consequences.

The modal is uncontrolled, so it hides itself regardless of where it was shown, with `modal.hide()` from inside as shown above. Nothing has to thread a callback down from the component that opened it.

The modal component's code is not executed when it is invisible. That is a performance claim with a specific meaning: a hidden modal is not a mounted component tree, so it costs nothing to keep registered. It also means anything your dialog component does on mount, including data fetching for its initial state, does not happen until the first time someone opens it.

`modal.remove()` is the counterpart, and it is the one people forget. Hiding is not removing, and the Ant.Design example wires `afterClose` to `modal.remove()` so the entry is unregistered once the exit animation finishes. A dialog that hides but never removes stays in the registry indefinitely.

Set against that, the library does nothing about accessibility, focus management or stacking multiple modals. Those belong to the UI library you plug in, and if your dialog component does not handle them, this one will not either.

## The last release was 1.2.13 in 2023

The maintenance picture is mixed and needs stating precisely. The repository is not archived and the last push was on 2026-09-09. The most recent releases are 1.2.13 on 2023-10-03, 1.2.12 on 2023-10-02 and 1.2.11 on 2023-09-29, and `package.json` in the repository root still reads 1.2.13.

So the source receives commits while the published package has not changed in roughly three years. Anything you read in the repository beyond 1.2.13 is unreleased, and there is no changelog entry in the README distinguishing the two.

The toolchain points the same direction. The development dependencies pin `@types/react` to `^17.0.13` and `@types/react-dom` to `^17.0.8`, with `@testing-library/react` at `^12.0.0` and `@testing-library/react-hooks` at `^7.0.1`. Those are React 17 era packages. Nothing in the listed dependencies or peer declarations indicates React 18 or 19 support.

The build itself is a plain dual tsc emit, with an ESM build and a CommonJS build pointed at separate output directories.

```json
"build": "rm -rf lib && yarn build:esm && yarn build:cjs",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts"
```

Note the `rm -rf` at the front of the build script. It works in a POSIX shell and fails in a plain Windows command prompt, so building this repository from source on Windows needs a shell that provides it. Shipping the package is unaffected, and the licence is MIT with a `LICENSE.md` in the root.

## Conclusion

Adopt nice-modal-react when the same dialog is opened from several unrelated components and a component library already owns your dialog rendering, since the id indirection and the promise are the whole reason to take this dependency. Do not adopt it for accessibility, focus handling or stacking, none of which it touches, and do not expect a resolved value from the promise. Verify first that version 1.2.13 works on your React version, because the last publish was 2023-10-03 while the toolchain in the repository still targets React 17 types.

## FAQ

### Does nice-modal-react include its own modal component?

No. The README is explicit that it is not a React modal component and is meant to be paired with a dialog implementation from a UI library such as Material UI, Ant.Design or Bootstrap React. It supplies state management only, and is about 2kb after gzip with zero dependencies.

### How do I show a modal with nice-modal-react?

Wrap your dialog with NiceModal.create, read state through the useModal hook, then call NiceModal.show(MyModal, { someProp: 'hello' }) at the call site. The returned promise resolves when the task inside the modal finishes.

### What is the difference between modal.hide() and modal.remove()?

hide closes the dialog while leaving it registered, and the Ant.Design example wires it to onOk and onCancel. remove unregisters it, and the example calls it in afterClose so the exit animation finishes first.

### Can I show a modal without importing its component?

Yes. Call NiceModal.register with an id and the component once at module level, then use NiceModal.show('my-modal', { someProp: 'hello' }) anywhere else. The README lists this decoupling as a key feature.

### When was the last version of nice-modal-react published?

Version 1.2.13 on 2023-10-03, after 1.2.12 on 2023-10-02 and 1.2.11 on 2023-09-29. The repository itself is not archived and the last push was on 2026-09-09, so commits land after the published version.

### Which React versions does nice-modal-react support?

The published package does not declare a React peer range in the listed package.json, and the development dependencies pin @types/react to ^17.0.13 with @testing-library/react at ^12.0.0, which are React 17 era packages.

## Sources

- [eBay/nice-modal-react on GitHub](https://github.com/eBay/nice-modal-react)
- [License: MIT](https://github.com/eBay/nice-modal-react/blob/main/LICENSE)
- [Project website](https://ebay.github.io/nice-modal-react)
- [README](https://github.com/eBay/nice-modal-react/blob/main/README.md)
- [Releases](https://github.com/eBay/nice-modal-react/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ebay-nice-modal-react
