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

@slithy/modal-kit

v0.5.2

Published

Headless modal React components for @slithy/modal-core.

Readme

@slithy/modal-kit

The headless React layer for the @slithy modal system. Provides primitives, behavior hooks, and a renderer — everything needed to build a fully functional modal UI with no animation dependency.

modal-kit + modal-core are sufficient for a production modal system. Add @slithy/modal-spring (or a custom adapter) only if you need animated transitions.


Installation

pnpm add @slithy/modal-core @slithy/modal-kit

Minimal Setup (no animation)

import { useModalStore } from '@slithy/modal-core'
import { Modal, ModalRenderer } from '@slithy/modal-kit'

// Render once at your app root
export function App() {
  const { backdropId, modals } = useModalStore(({ backdropId, modals }) => ({
    backdropId,
    modals,
  }))
  const showBackdrop = !!backdropId && modals.length > 0

  return (
    <>
      <main>{/* your app */}</main>
      <ModalRenderer
        renderBackdrop={() => showBackdrop ? <ModalBackdrop /> : null}
      />
    </>
  )
}

// Open a modal from anywhere
useModalStore.getState().openModal(
  <Modal aria-label="My Modal">
    <p>Content</p>
  </Modal>,
  { triggerEvent: event }
)

Primitives

These are the building blocks. Each can be composed directly or wrapped by animation adapters.

ModalContainer

Fixed-position layout shell. Handles horizontal alignment.

<ModalContainer alignX="center" modalId={id}>
  {/* ModalContent, etc. */}
</ModalContainer>

| Prop | Type | Default | |---|---|---| | alignX | 'center' \| 'left' \| 'right' | 'center' | | modalId | string | — | | onBackdropClick | () => void | — | | className | string | — |

ModalContent

Content wrapper. Controls vertical alignment, pointer events during close, and the data-disable-opacity toggle.

<ModalContent alignY="middle" modalState={modalState} disableOpacityTransition>
  {/* ModalDialog, etc. */}
</ModalContent>

| Prop | Type | Default | |---|---|---| | alignY | 'middle' \| 'top' \| 'bottom' | — | | modalState | ModalState | — | | disableOpacityTransition | boolean | — | | style | CSSProperties | — |

ModalDialog

The <dialog open> element with correct ARIA attributes, focus target, and modal-dialog styles.

<ModalDialog
  ref={contentRef}
  aria-label="My Modal"
  modalId={modalId}
  onKeyDown={onKeyDown}
>
  {children}
</ModalDialog>

| Prop | Type | |---|---| | aria-label | string | | className | string | | modalId | string | | onKeyDown | (e: KeyboardEvent<HTMLDialogElement>) => void | | style | CSSProperties |

ModalBackdrop

Fixed-position backdrop with click handling via usePointerClick.

<ModalBackdrop onClick={handleClose} />

| Prop | Type | |---|---| | onClick | () => void | | className | string | | style | CSSProperties |

ModalVeil

Absolute-position overlay that dims a backgrounded modal when another modal opens in front of it. A pure visual primitive — store logic lives in the consuming Modal.

<ModalVeil style={style} />

| Prop | Type | |---|---| | style | CSSProperties |


Components

Modal

Reference implementation. Composes all four primitives into a functional non-animated modal. Use it directly for a no-animation setup, or as a reference when building a custom adapter.

<Modal
  aria-label="Settings"
  alignX="center"
  alignY="middle"
  contentClassName="my-card"
  contentStyle={{ borderRadius: 12 }}
  dismissible
>
  {children}
</Modal>

| Prop | Type | Default | Description | |---|---|---|---| | aria-label | string | — | Accessible name for the dialog | | alignX | 'center' \| 'left' \| 'right' | 'center' | Horizontal position | | alignY | 'middle' \| 'top' \| 'bottom' | 'middle' | Vertical position | | children | ReactNode | — | — | | contentClassName | string | — | Class on the <dialog> element | | contentStyle | CSSProperties | — | Inline styles on the <dialog> element | | disableOpacityTransition | boolean | — | Skip the default opacity fade | | dismissible | boolean | true | Allow Escape and backdrop-click to close | | onLeave | (done: () => void) => void | — | Called when close begins; call done() after leave animation | | afterOpen | () => void | — | Fires after mount | | afterClose | () => void | — | Fires after modal is removed |

ModalRenderer

Renders all open modals from the store. Place once at the app root.

<ModalRenderer
  renderBackdrop={() => showBackdrop ? <ModalBackdrop /> : null}
  renderLayer={(children) => <LayerProvider>{children}</LayerProvider>}
  renderPortal={(children) => <Portal>{children}</Portal>}
/>

| Prop | Type | Description | |---|---|---| | renderBackdrop | () => ReactNode | Backdrop; caller controls visibility and animation | | renderLayer | (children) => ReactNode | Wrap all modals in a layer context | | renderPortal | (children) => ReactNode | Wrap each modal in a portal | | skipAnimation | boolean | Skip all modal animations. Intended for test environments |


Styling

The <dialog> element is the primary styling target. Three approaches are available, and can be combined.

contentClassName

Pass a class name to the dialog for variant-level styles:

<Modal contentClassName="my-modal">...</Modal>
.my-modal {
  border-radius: 12px;
  max-width: 480px;
}

contentStyle

Pass inline styles for one-off overrides:

<Modal contentStyle={{ maxWidth: 640 }}>...</Modal>

data-modalid (per-instance scoping)

Every <dialog> receives a data-modalid attribute set to the modal's store ID. Use it to scope styles to a specific instance — useful with React 19's <style href> deduplication when multiple modal types are open simultaneously:

function MyModal() {
  const modalId = useModalState((s) => s.modalId);

  return (
    <Modal aria-label="Settings">
      <style href={`modal-${modalId}`} precedence="component">{`
        [data-modalid="${modalId}"] {
          border-radius: 12px;
          & .modal-header { font-size: 1.25rem; }
        }
      `}</style>
      {children}
    </Modal>
  );
}

Because href is derived from the runtime modalId, each open modal gets its own deduplicated style block, and styles from different modal types never collide.


CSS Variables

--modalBackdropColor

Controls the background color of ModalBackdrop and ModalVeil. Set it anywhere in the cascade — both components use it as a fallback-safe var().

/* default */
--modalBackdropColor: rgba(0, 0, 0, 0.5);
/* override per-page or per-modal */
:root {
  --modalBackdropColor: rgba(15, 10, 30, 0.6);
}

Hooks

useModalLogic

The core modal hook. Manages registration, lifecycle, focus, and Escape key. Called by Modal and by animation adapters.

const {
  contentRef,       // ref to attach to <dialog>
  handleCloseModal, // call to begin the close sequence
  isTopModal,       // whether this is the frontmost modal
  layerIsActive,    // whether this modal's layer is active
  markAtRest,       // call once enter animation completes to unblock closing
  modalId,          // this modal's store ID
  modalState,       // 'opening' | 'open' | 'closing' | 'closed'
  skipAnimation,    // whether animations are skipped (from ModalRenderer prop)
} = useModalLogic({
  afterClose,
  layerIsActive,    // pass from useLayerState for full layer coordination
  manageFocus,      // false to skip focus management (e.g. adapter handles its own trap)
  onLeave,          // (done) => void — call done() after leave animation
})

useModalState

Reads per-modal context set by ModalProvider. Use this inside any component rendered as a modal to access the modal's own ID and state.

const modalId = useModalState((s) => s.modalId)
const modalState = useModalState((s) => s.state)

| Field | Type | Description | |---|---|---| | modalId | string | This modal's store ID | | state | ModalState | 'opening' \| 'open' \| 'closing' \| 'closed' | | enqueuedToClose | boolean | Whether a close has been requested | | skipAnimation | boolean \| undefined | From ModalRenderer prop | | triggerElement | HTMLElement \| undefined | The element that triggered this modal |

Must be called inside a component rendered within ModalRenderer. The most common use is reading modalId to pass to closeModal:

const modalId = useModalState((s) => s.modalId)
useModalStore.getState().closeModal(modalId)

useDialogKeyDown

Composes the Escape-key close handler and useTrapFocus into a single onKeyDown for a <dialog>.

const onKeyDown = useDialogKeyDown({
  dismissible,
  handleCloseModal,
  isTopModal,
  layerIsActive,
  onKeyDown: trapFocus.onKeyDown,
})

Building an Adapter

An adapter:

  1. Calls useModalLogic and passes onLeave for leave animation control
  2. Wraps ModalContent, ModalDialog, etc. with its animation library (e.g. animated(ModalDialog))
  3. Calls markAtRest() when the enter animation completes (unblocks closing and transitions state to 'open')
  4. Calls afterOpen?.() alongside markAtRest() in the enter onRest callback
  5. Calls done() in the leave onRest callback to remove the modal
  6. Wraps ModalRenderer with renderBackdrop to inject an animated backdrop

See @slithy/modal-spring for a complete example.


Exports

| Export | Description | |---|---| | Modal | Reference non-animated modal component | | ModalBackdrop | Fixed-position backdrop primitive | | ModalContainer | Layout shell primitive | | ModalContent | Content wrapper primitive | | ModalDialog | <dialog> element primitive | | ModalRenderer | Renders all open modals from the store | | ModalVeil | Absolute-position veil overlay primitive | | useDialogKeyDown | Escape + trapFocus key handler hook | | useModalLogic | Core lifecycle and behavior hook | | useModalState | Per-modal context hook (ID, state, triggerElement) | | ModalBackdropProps | — | | ModalContentProps | — | | ModalDialogProps | — | | ModalProps | — | | ModalRendererProps | — | | ModalVeilProps | — | | UseModalLogicOptions | — | | UseModalLogicResult | — |