Guide: Stacking Modals with Layers

Real apps open modals from inside modals: a settings dialog that asks for confirmation, or a form that opens a picker. This guide explains how React Snappy Modal orders stacked modals and how to close the one you mean.

Three different settings

Stacked modals involve three settings that are easy to mix up:

SettingWhat it isUse it to
layerA number option, 0 by default. Open modals are sorted by layer, and higher layers render later, on top.Order groups of modals and target a group with close(value, layer).
modalIdA unique ID created for each show() call, available from useCurrentModal().Close one specific modal.
zIndexThe CSS z-index of the modal area, 1000 by default.Place modals above other page elements such as sticky headers.

The common mistake: closing by layer

Every modal opens on layer 0 unless you pass another one. If a modal opens a second modal and the second calls SnappyModal.close(), the library looks for the first modal on layer 0, which is the parent. The parent closes and the child stays on screen.

tsx
1function Child() {
2  // Looks for the first modal on layer 0 — that is the Parent
3  return <button onClick={() => SnappyModal.close('child done')}>Done</button>;
4}
5
6function Parent() {
7  return <button onClick={() => SnappyModal.show(<Child />)}>Open child</button>;
8}
9
10SnappyModal.show(<Parent />); // layer 0
11// Clicking "Done" closes Parent; Child stays on screen

Fix 1: close from inside with useCurrentModal

resolveModal() targets the modal it was rendered in by ID, so it is correct no matter how many modals share a layer. Make it the default in every modal component. The parent can also await the child and close itself only after the child answers.

tsx
1function Child() {
2  const { resolveModal } = useCurrentModal();
3  return <button onClick={() => resolveModal('child done')}>Done</button>;
4}
5
6function Parent() {
7  const { resolveModal } = useCurrentModal();
8
9  const openChild = async () => {
10    const answer = await SnappyModal.show(<Child />);
11    if (answer) resolveModal(answer); // close Parent only after Child answered
12  };
13
14  return <button onClick={openChild}>Open child</button>;
15}

Fix 2: give nested modals their own layer

When you have to close a modal from outside, for example from a timer or a global keyboard shortcut, open each level on its own layer and pass that layer to close(). Naming the layers in one constant keeps the order readable.

tsx
1const LAYER = { page: 0, picker: 1, alert: 2 } as const;
2
3SnappyModal.show(<ColorPicker />, { layer: LAYER.picker });
4
5// Later, from anywhere (a timer, a keyboard shortcut, ...)
6SnappyModal.close(undefined, LAYER.picker);

Fix 3: close a specific modal by ID

useCurrentModal() also returns modalId. Have the modal report it from an effect, wait until it arrives, then pass it to SnappyModal.close(value, modalId) to close exactly that modal, even if others share its layer. Don't close before the ID is known: an empty ID is read as layer 0 and closes the first modal there.

tsx
1function UploadProgress({ onReady }: { onReady: (id: string) => void }) {
2  const { modalId } = useCurrentModal();
3  useEffect(() => onReady(modalId), [modalId, onReady]);
4  return <p>Uploading…</p>;
5}
6
7const opened = Promise.withResolvers<string>();
8SnappyModal.show(<UploadProgress onReady={opened.resolve} />, { allowOutsideClick: false });
9
10// Wait for the ID too: closing with an empty ID would target layer 0 instead
11const [modalId] = await Promise.all([opened.promise, upload(file)]);
12SnappyModal.close('uploaded', modalId);

Outside clicks and scroll lock with several modals

An outside click closes the modal whose area was clicked, by ID, and resolves its Promise with undefined. Modals underneath stay open.

The page stays locked while any open modal has allowScroll: false. It unlocks and restores the scroll position only after the last such modal closes.

Reacting to the stack

useSnappyModalState() re-renders whenever the stack changes. Use it to pause global keyboard shortcuts while a modal is open, or to show how many dialogs are stacked.

tsx
1function useEscapeToGoBack() {
2  const { isShow } = useSnappyModalState();
3
4  useEffect(() => {
5    if (isShow) return; // let the open modal handle the keyboard
6    const onKey = (event: KeyboardEvent) => {
7      if (event.key === 'Escape') history.back();
8    };
9    window.addEventListener('keydown', onKey);
10    return () => window.removeEventListener('keydown', onKey);
11  }, [isShow]);
12}

Checklist

When a screen can open more than one modal, check that:

  • Modal components close themselves with useCurrentModal().
  • SnappyModal.close() without arguments is only used when a single modal can be open.
  • Modals closed from outside have their own layer or are closed by modalId.
  • zIndex is only used to rise above non-modal page elements, not to order modals.