npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ivex0002/stack-modal-presets

v1.0.4

Published

Preset layouts for @ivex0002/stack-modal library

Readme

@ivex0002/stack-modal

A flexible and customizable modal stack management library for React applications. Modal Stack allows you to create layered modals with smooth animations and various preset styles.

Preview

default preset

Honeycam 2025-12-28 13-23-52

custom layout style (tailwind, framer-motion)

Honeycam 2025-12-28 13-24-38

test project link

https://github.com/Ivex0002/stack-modal-test-project

Features

  • 🎨 Multiple Presets: Default, Minimal, and Drawer layouts
  • 📚 Modal Stacking: Support for multiple modals with depth management
  • ⌨️ Keyboard Support: ESC key to close modals
  • 🎭 Custom Layouts: Create your own modal layouts
  • 🪶 Lightweight: Minimal dependencies
  • 💪 TypeScript: Full type safety
  • 🔄 React 18 Compatible: Built with useSyncExternalStore
  • 🎯 Zero runtime dependencies (React as peer dependency)

Installation

Core Package

npm install @ivex0002/stack-modal

Presets (Optional)

If you want to use built-in presets:

npm install @ivex0002/stack-modal-presets

Note: Presets are optional. If you're creating custom layouts, you don't need to install this presets package.

Quick Start

import { createStackModal } from "@ivex0002/stack-modal";
import { defaultPreset } from "@ivex0002/stack-modal-presets";

// Define your modals
const modals = {
  alert: ({ message }: { message: string }) => (
    <div>
      <h2>Alert</h2>
      <p>{message}</p>
    </div>
  ),
  confirm: ({ title, onConfirm }: { title: string; onConfirm: () => void }) => (
    <div>
      <h2>{title}</h2>
      <button onClick={onConfirm}>Confirm</button>
    </div>
  ),
};

// Create modal instance with preset
const modal = createStackModal(modals, defaultPreset);

// Use in your component
function App() {
  return (
    <div>
      <button onClick={() => modal.alert.push({ message: "Hello!" })}>
        Show Alert
      </button>
      <button
        onClick={() =>
          modal.confirm.push({
            title: "Are you sure?",
            onConfirm: () => console.log("Confirmed!"),
          })
        }
      >
        Show Confirm
      </button>
    </div>
  );
}

Presets

Default Preset

A centered modal with stacking animation. Multiple modals are displayed with offset and scale effects.

import { defaultPreset } from "@ivex0002/stack-modal-presets";

const modal = createStackModal(modals, defaultPreset);

Minimal Preset

A simple centered modal with fade transitions. Only the top modal is visible.

import { minimalPreset } from "@ivex0002/stack-modal-presets";

const modal = createStackModal(modals, minimalPreset);

Drawer Preset

A bottom sheet style modal that slides up from the bottom of the screen.

import { drawerPreset } from "@ivex0002/stack-modal-presets";

const modal = createStackModal(modals, drawerPreset);

API Reference

createStackModal(modals, modalLayout)

Creates a modal instance with the specified modals and layout.

Parameters:

  • modals: An object where keys are modal names and values are React components
  • modalLayout: A layout configuration object (use presets or create custom)

Returns:

  • Modal control object with push methods for each modal and a pop method

Modal Control Object

const modal = createStackModal(modals, preset);

// Push a modal
modal.modalName.push(props);

// Close the top modal
modal.pop();

Custom Layout

You can create your own modal layout by implementing the ModalLayout interface.

this example made with tailwindcss & framer-motion.

import { motion } from "framer-motion";
import React from "react";
import type { ModalLayout } from "@ivex0002/stack-modal";

const STACK_OFFSET = 80;
const SCALE_STEP = 0.06;
const OPACITY_STEP = 0.08;

export const twfmModalLayoutExample: ModalLayout = {
  Background: ({
    children,
    onClose,
  }: {
    children: React.ReactNode;
    onClose: () => void;
  }) => {
    React.useEffect(() => {
      const onKey = (e: KeyboardEvent) => {
        if (e.key === "Escape") onClose();
      };
      window.addEventListener("keyup", onKey);
      return () => window.removeEventListener("keyup", onKey);
    }, [onClose]);

    return (
      <motion.div
        className="fixed inset-0 z-1000 flex items-center justify-center backdrop-blur-md"
        style={{
          background:
            "linear-gradient(to bottom right, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2), rgba(236, 72, 153, 0.2))",
        }}
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        transition={{ duration: 0.3 }}
        onClick={onClose}
      >
        <motion.div
          className="absolute inset-0 bg-black/40"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
        <div onClick={(e) => e.stopPropagation()}>{children}</div>
      </motion.div>
    );
  },

  ModalWrap: ({
    children,
    depth,
    isTop,
  }: {
    children: React.ReactNode;
    depth: number;
    isTop: boolean;
  }) => {
    const x = -depth * STACK_OFFSET;
    const scale = 1 - depth * SCALE_STEP;
    const opacity = 1 - depth * OPACITY_STEP;

    return (
      <motion.div
        className="absolute w-auto max-w-[90vw] rounded-3xl bg-white p-8 ring-1 ring-black/5"
        style={{
          left: "50%",
          top: "50%",
          pointerEvents: isTop ? "auto" : "none",
          background:
            "linear-gradient(to bottom right, white, white, rgb(249, 250, 251))",
          boxShadow: isTop
            ? "0 20px 70px -10px rgba(0,0,0,0.3), 0 0 0 1px rgba(99,102,241,0.1)"
            : "0 10px 40px -10px rgba(0,0,0,0.2)",
        }}
        initial={{
          opacity: 0,
          scale: 0.9,
          x: "-50%",
          y: "-45%",
        }}
        animate={{
          opacity,
          scale,
          x: `calc(-50% + ${x}px)`,
          y: "-50%",
        }}
        exit={{
          opacity: 0,
          scale: 0.9,
          x: "-50%",
          y: "-45%",
        }}
        transition={{
          type: "spring",
          stiffness: 280,
          damping: 28,
          mass: 0.8,
        }}
      >
        <div
          className="absolute inset-0 rounded-3xl pointer-events-none"
          style={{
            background:
              "linear-gradient(to bottom right, rgba(99, 102, 241, 0.05), transparent, rgba(168, 85, 247, 0.05))",
          }}
        />
        <div
          className="absolute -top-px left-1/2 -translate-x-1/2 w-1/2 h-px"
          style={{
            background:
              "linear-gradient(to right, transparent, rgba(99, 102, 241, 0.5), transparent)",
          }}
        />

        <div className="relative z-10">{children}</div>
      </motion.div>
    );
  },
};

const modal = createStackModal(modals, twfmModalLayoutExample);

ModalLayout Interface

type ModalLayout = {
  Background: React.ComponentType<{
    children: React.ReactNode;
    onClose: () => void;
  }>;
  ModalWrap: React.ComponentType<{
    children: React.ReactNode;
    depth: number;
    isTop: boolean;
  }>;
};
  • Background: The backdrop and container for all modals

    • onClose: Function to close the top modal
    • children: The modal content
  • ModalWrap: Wrapper for each individual modal

    • depth: The position in the stack (0 = top, 1 = second from top, etc.)
    • isTop: Boolean indicating if this is the topmost modal
    • children: The modal content

Examples

Alert Modal

const modals = {
  alert: ({ message }: { message: string }) => (
    <div style={{ padding: "20px" }}>
      <h2>Alert</h2>
      <p>{message}</p>
      <button onClick={() => modal.pop()}>OK</button>
    </div>
  ),
};

// Usage
modal.alert.push({ message: "Something happened!" });

Confirmation Modal

const modals = {
  confirm: ({
    title,
    message,
    onConfirm,
  }: {
    title: string;
    message: string;
    onConfirm: () => void;
  }) => (
    <div style={{ padding: "20px" }}>
      <h2>{title}</h2>
      <p>{message}</p>
      <div style={{ display: "flex", gap: "10px" }}>
        <button
          onClick={() => {
            onConfirm();
            modal.pop();
          }}
        >
          Confirm
        </button>
        <button onClick={() => modal.pop()}>Cancel</button>
      </div>
    </div>
  ),
};

// Usage
modal.confirm.push({
  title: "Delete Item",
  message: "Are you sure you want to delete this item?",
  onConfirm: () => console.log("Item deleted"),
});

Stacked Modals

// Open multiple modals
modal.first.push({ data: "First modal" });
modal.second.push({ data: "Second modal" });
modal.third.push({ data: "Third modal" });

// Close them one by one
modal.pop(); // Closes third
modal.pop(); // Closes second
modal.pop(); // Closes first

TypeScript Support

Modal Stack is written in TypeScript and provides full type safety:

// Props are fully typed
const modals = {
  user: ({ name, age }: { name: string; age: number }) => (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  ),
};

const modal = createStackModal(modals, defaultPreset);

// ✅ Correct
modal.user.push({ name: "John", age: 30 });

// ❌ TypeScript error
modal.user.push({ name: "John" }); // Missing 'age'
modal.user.push({ name: "John", age: "30" }); // Wrong type

How It Works

Modal Stack uses a simple but powerful architecture to manage modal states and rendering.

Core Components

1. Stack Store

The store manages the modal stack state using a simple array:

type StackItem = {
  key: string;
  props: unknown;
};

export const createStackStore = () => {
  let stack: StackItem[] = [];
  const listeners = new Set<() => void>();
  return {
    push(item: StackItem) {
      stack = [...stack, item];
      listeners.forEach((l) => l());
    },
    pop() {
      stack = stack.slice(0, -1);
      listeners.forEach((l) => l());
    },
    get() {
      return stack;
    },
    subscribe(fn: () => void) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
  };
};

2. Modal Renderer

The ModalRender component subscribes to the store and renders the modal stack:

type ModalRegistry = {
  [key: string]: (props?: unknown) => React.ReactNode;
};
type ModalLayout = {
  Background: React.ComponentType<{
    children: React.ReactNode;
    onClose: () => void;
  }>;
  ModalWrap: React.ComponentType<{
    children: React.ReactNode;
    depth: number;
    isTop: boolean;
  }>;
};

function ModalRender<M extends ModalRegistry>({
  modals,
  modalLayout,
  store,
}: {
  modals: M;
  modalLayout: ModalLayout;
  store: StackStore;
}) {
  // Subscribe to store changes using React 18's useSyncExternalStore
  const stack = React.useSyncExternalStore(
    store.subscribe, // Subscribe function
    store.get, // Get snapshot (client)
    store.get // Get snapshot (server)
  );

  if (stack.length === 0) return null;

  return (
    <modalLayout.Background onClose={() => store.pop()}>
      {stack.map((item, index) => {
        // Calculate depth: top modal = 0, second = 1, etc.
        const depth = stack.length - 1 - index;

        return (
          <modalLayout.ModalWrap
            depth={depth}
            isTop={index === stack.length - 1}
            key={index}
          >
            {/* Dynamically render the modal component */}
            {modals[item.key](item.props)}
          </modalLayout.ModalWrap>
        );
      })}
    </modalLayout.Background>
  );
}

Key features:

  • Uses useSyncExternalStore for optimal performance and React 18 compatibility
  • Calculates depth for each modal (0 = top, higher = deeper in stack)
  • Identifies the isTop modal for interaction control
  • Dynamically renders modal components based on the stack state

3. Modal Creation Flow

// 1. Define modals
const modals = {
  alert: (props) => <AlertComponent {...props} />,
  confirm: (props) => <ConfirmComponent {...props} />,
};

// 2. Create modal instance
const modal = createStackModal(modals, preset);

// This creates:
// - A store to manage modal stack
// - A root DOM element to render modals
// - Push methods for each modal type
// - A pop method to close modals

// 3. Use modals
modal.alert.push({ message: "Hello" });
// -> Adds { key: "alert", props: { message: "Hello" } } to stack
// -> Triggers re-render
// -> ModalRender displays the modal

modal.pop();
// -> Removes top item from stack
// -> Triggers re-render
// -> Modal disappears

Rendering Process

  1. Initial State: Stack is empty, nothing renders
  2. Push Modal:
    • Modal data added to stack
    • Listeners notified
    • ModalRender re-renders with new stack
    • Background and ModalWrap components render the modal
  3. Stack Multiple Modals:
    • Each modal gets a depth value
    • Presets use depth to position/style modals
    • Only top modal (isTop={true}) receives pointer events
  4. Pop Modal:
    • Top item removed from stack
    • Component re-renders
    • If stack is empty, returns null

Why useSyncExternalStore?

useSyncExternalStore is a React 18 hook that safely subscribes to external stores:

  • Tearing prevention: Ensures consistent state across concurrent renders
  • SSR support: Separate snapshots for server and client
  • Performance: Only re-renders when subscribed state changes

Troubleshooting

Modals not appearing?

Make sure createStackModal is called at the module level, not inside a component:

// ✅ Correct
const modal = createStackModal(modals, preset);

function App() {
  return <button onClick={() => modal.alert.push()}>Open</button>;
}

// ❌ Wrong - creates new instance on every render
function App() {
  const modal = createStackModal(modals, preset);
  return <button onClick={() => modal.alert.push()}>Open</button>;
}

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.