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

@xsolla/xui-modal

v0.173.2

Published

A cross-platform React modal dialog system. A provider/context/hook trio drives a stack of dialogs supporting three presentation types (`popup`, `bottom-sheet`, `full-screen`), structured headers, focus trapping, and programmatic open/close on both web an

Readme

Modal

A cross-platform React modal dialog system. A provider/context/hook trio drives a stack of dialogs supporting three presentation types (popup, bottom-sheet, full-screen), structured headers, focus trapping, and programmatic open/close on both web and React Native.

Installation

npm install @xsolla/xui-modal

Imports

import {
  Modal,
  ModalProvider,
  ModalContext,
  WorkArea,
  useModal,
  useModalId,
  type ModalProps,
  type ModalSize,
  type ModalVariant,
  type WorkAreaProps,
  type WorkAreaSize,
} from "@xsolla/xui-modal";

useModalId is re-exported from @xsolla/xui-core and is consumed by portaled descendants (e.g. Select, Dropdown) so click-outside detection ignores content that logically belongs to the modal.

Quick start

import * as React from "react";
import { Modal, ModalProvider, useModal } from "@xsolla/xui-modal";
import { Button } from "@xsolla/xui-button";

function Trigger() {
  const [open, close] = useModal(() => (
    <Modal onClose={close} title="Confirm action">
      <p>Are you sure you want to proceed?</p>
      <Button onPress={close}>Close</Button>
    </Modal>
  ));
  return <Button onPress={open}>Open modal</Button>;
}

export default function QuickStart() {
  return (
    <ModalProvider>
      <Trigger />
    </ModalProvider>
  );
}

API Reference

<Modal>

The dialog component with header, body, and footer zones. Accepts a ref forwarded to the inner WorkArea. Unknown props are spread onto the root Box. size is a typed ModalProps field but is never consumed by Modal's internal layout; it passes through via ...rest to the root Box with no built-in styling effect.

| Prop | Type | Default | Description | | ------------------ | -------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. | | children | ReactNode | — | Required. Modal body content. | | type | "popup" \| "bottom-sheet" \| "full-screen" | "popup" | Presentation type. Drives positioning, border-radius, and shadow. | | align | "left" \| "center" | "left" | Content alignment within the body. | | onClose | () => void | — | Close callback. When provided, renders a close (X) button and enables Escape and click-outside dismissal. | | onBack | () => void | — | Back callback. Renders a chevron-left button on the left of the header. | | header | ReactNode | — | Custom header — replaces the default back/close row. | | footer | ReactNode | — | Footer content rendered below children. | | openContent | boolean | false | Removes inner padding so children fill edge-to-edge. | | closeOutside | boolean | true | Whether clicking outside closes the modal (requires onClose). | | maxWidth | string \| number | 680 | Forced to 100% for bottom-sheet and full-screen. | | minHeight | string \| number | — | Minimum height of the modal. | | styled | CSSProperties | — | Inline overrides on the modal container. | | title | string | — | Visually hidden accessible title — drives aria-labelledby. For a visible heading, render it inside children or pass a custom header. | | aria-label | string | — | Alternative accessible name when title is not used. | | aria-describedby | string | — | ID of an element describing modal content. | | initialFocusRef | RefObject<HTMLElement> | — | Element to focus on open. Falls back to the close button, then the first focusable child. |

Inherits ThemeOverrideProps (themeMode, themeProductContext).

Modal types

| Type | Positioning | Border radius | Shadow | Max width | | ----------------- | ---------------------- | ---------------- | ------ | ------------------- | | popup (default) | Centred in the overlay | All corners | Yes | 680px (or custom) | | bottom-sheet | Anchored to the bottom | Top corners only | Yes | 100% (forced) | | full-screen | Fills the viewport | 0 | None | 100% (forced) |

<ModalProvider>

Wraps your app, manages the open-modal stack, and renders ModalRoot.

| Prop | Type | Description | | ---------- | ----------- | ---------------------------------- | | children | ReactNode | Required. Application content. |

<WorkArea>

Internal chrome used by Modal. Can be rendered standalone for custom layouts.

| Prop | Type | Default | Description | | ------------- | -------------------------------------------- | --------- | --------------------------------------------------- | | children | ReactNode | — | Required. Content. | | type | "popup" \| "bottom-sheet" \| "full-screen" | "popup" | Affects border-radius and shadow. | | align | "left" \| "center" | — | Content alignment. | | indent | "sm" \| "md" \| "lg" | — | Padding hint passed alongside the work-area sizing. | | stretched | boolean | false | Stretch to full height. | | openContent | boolean | false | Edge-to-edge mode. | | fetching | boolean | false | Renders a loading placeholder when true. |

Inherits ThemeOverrideProps (themeMode, themeProductContext).

useModal(renderFn)

Returns [open, close] for a single modal. Must be called inside a ModalProvider.

function useModal(modal: (props: any) => ReactNode): [() => void, () => void];

The hook generates a stable random key per component instance and keeps a ref to the latest render function — your closure values stay current without a dependency array.

useModalId()

Returns the current modal's data-modal-id. Portaled descendants should set this attribute on their root so the modal recognises them as in-tree for click-outside checks.

ModalContext

The raw React context. Most apps consume useModal, but advanced cases can call onOpenModal(key, renderFn) and onCloseModal(key) directly.

Exported types

| Type | Members | | -------------------- | ------------------------------------------------------------ | | ModalProps | Props for <Modal>. | | ModalSize | "sm" \| "md" \| "lg" | | ModalVariant | "popup" \| "bottom-sheet" \| "full-screen" | | ModalType | (props: any) => ReactNode — the render function signature. | | ModalContextType | { onOpenModal, onCloseModal } | | ModalProviderProps | Props for <ModalProvider>. | | ModalRootProps | Props for the internal root. | | WorkAreaProps | Props for <WorkArea>. | | WorkAreaSize | "sm" \| "md" \| "lg" |

Examples

Modal types

import * as React from "react";
import { Modal, ModalProvider, useModal } from "@xsolla/xui-modal";
import { Button } from "@xsolla/xui-button";

function Demo() {
  const [openPopup, closePopup] = useModal(() => (
    <Modal type="popup" onClose={closePopup} title="Popup">
      <p>Centred with border-radius and shadow.</p>
    </Modal>
  ));
  const [openSheet, closeSheet] = useModal(() => (
    <Modal type="bottom-sheet" onClose={closeSheet} title="Bottom sheet">
      <p>Anchored to the bottom.</p>
    </Modal>
  ));
  const [openFull, closeFull] = useModal(() => (
    <Modal type="full-screen" onClose={closeFull} title="Full screen">
      <p>Fills the viewport.</p>
    </Modal>
  ));
  return (
    <div style={{ display: "flex", gap: 12 }}>
      <Button onPress={openPopup}>Popup</Button>
      <Button onPress={openSheet}>Bottom sheet</Button>
      <Button onPress={openFull}>Full screen</Button>
    </div>
  );
}

export default function Example() {
  return (
    <ModalProvider>
      <Demo />
    </ModalProvider>
  );
}

Confirmation dialog

import * as React from "react";
import { Modal, ModalProvider, useModal } from "@xsolla/xui-modal";
import { Button, ButtonGroup } from "@xsolla/xui-button";

function ConfirmTrigger() {
  const [confirmed, setConfirmed] = React.useState(false);
  const [open, close] = useModal(() => (
    <Modal
      onClose={close}
      closeOutside={false}
      maxWidth={400}
      title="Delete item?"
      footer={
        <ButtonGroup orientation="horizontal" size="xl">
          <Button variant="secondary" tone="mono" onPress={close}>
            Cancel
          </Button>
          <Button
            tone="alert"
            onPress={() => {
              setConfirmed(true);
              close();
            }}
          >
            Delete
          </Button>
        </ButtonGroup>
      }
    >
      <p>This action cannot be undone.</p>
    </Modal>
  ));

  return (
    <div>
      <Button tone="alert" onPress={open}>
        Delete
      </Button>
      {confirmed && <p>Deleted.</p>}
    </div>
  );
}

export default function ConfirmDialog() {
  return (
    <ModalProvider>
      <ConfirmTrigger />
    </ModalProvider>
  );
}

Multi-step modal

import * as React from "react";
import { Modal, ModalProvider, useModal } from "@xsolla/xui-modal";
import { Button } from "@xsolla/xui-button";

function MultiStep() {
  const [step, setStep] = React.useState(1);
  const [open, close] = useModal(() => (
    <Modal
      onClose={() => {
        setStep(1);
        close();
      }}
      onBack={step > 1 ? () => setStep((s) => s - 1) : undefined}
      title={`Step ${step} of 3`}
    >
      <p>Step {step} content.</p>
      {step < 3 ? (
        <Button onPress={() => setStep((s) => s + 1)}>Next</Button>
      ) : (
        <Button
          onPress={() => {
            setStep(1);
            close();
          }}
        >
          Finish
        </Button>
      )}
    </Modal>
  ));
  return <Button onPress={open}>Open multi-step</Button>;
}

export default function MultiStepExample() {
  return (
    <ModalProvider>
      <MultiStep />
    </ModalProvider>
  );
}

Edge-to-edge content

import * as React from "react";
import { Modal, ModalProvider, useModal } from "@xsolla/xui-modal";
import { Button } from "@xsolla/xui-button";

function HeroTrigger() {
  const [open, close] = useModal(() => (
    <Modal openContent onClose={close} title="Gallery">
      <img
        src="/hero-image.jpg"
        alt="Hero"
        style={{ width: "100%", display: "block" }}
      />
    </Modal>
  ));
  return <Button onPress={open}>Open hero</Button>;
}

export default function EdgeToEdge() {
  return (
    <ModalProvider>
      <HeroTrigger />
    </ModalProvider>
  );
}

Platform Support

| Feature | Web | React Native | | ------------- | ----------------------------------------- | ---------------------------------------------- | | Provider | ModalProvider (with ModalRoot portal) | ModalProvider (root is a no-op stub) | | Hook | useModal()[open, close] | Same signature; provider stack does not render | | Overlay | Fixed-positioned backdrop | N/A | | Focus trap | Keyboard Tab trapping | N/A | | Escape key | Closes modal | N/A | | Click outside | Dismisses when closeOutside={true} | N/A |

Keyboard Interaction

| Key | Action | | ------------- | -------------------------------------------------------- | | Escape | Closes the modal (when onClose is provided). | | Tab | Moves focus to the next focusable element (trapped). | | Shift + Tab | Moves focus to the previous focusable element (trapped). | | Enter | Activates the focused button. |

Accessibility

  • Renders with role="dialog" and aria-modal="true".
  • title creates a visually hidden labelled element via aria-labelledby; otherwise aria-label is used.
  • Focus is trapped within the modal; on open it moves to initialFocusRef, the close button, or the first focusable element. On close, focus returns to the previously active element.
  • The default close button has aria-label="Close modal"; the back button has aria-label="Go back".
  • data-modal-id lets click-outside detection ignore portaled content (e.g. Select, Dropdown) belonging to the modal.

Troubleshooting

"useModal must be used within a ModalProvider"

useModal (or direct ModalContext access) ran outside a ModalProvider. Ensure your component tree has a ModalProvider ancestor.

Modal appears behind other content

ModalRoot uses z-index: 1000 via portal into document.body. Toast notifications use z-index: 9999 and will appear above modals by default.

Click-outside not working for portaled dropdowns

Portaled descendants must set data-modal-id (read via useModalId) on their root so the modal recognises them as in-tree.

Bottom-sheet or full-screen modal not filling width

maxWidth is forced to 100% for these types — check that no parent container is restricting width.

Focus not returning after close

The modal saves document.activeElement on mount. If the trigger element is removed from the DOM while the modal is open, focus cannot be restored.