react ~11 KB 0 deps v0.4.1 β†— GitHub β†—

@mshafiqyajid/react-modal

Headless modal hook and styled dialog/drawer for React. Focus trap, scroll lock, Escape to close, four drawer positions, four sizes, smooth enter/exit animations, confirm variant, mobile sheet mode, swipe-to-dismiss, modal stacking with depth-aware z-index, async preventClose guard, ARIA-compliant.

Playground #

Props
TSX
import { ModalStyled } from "@mshafiqyajid/react-modal/styled";
import "@mshafiqyajid/react-modal/styles.css";

<ModalStyled
  isOpen={open}
  onClose={() => setOpen(false)}
  title="Example modal"
/>

Install #

npm install @mshafiqyajid/react-modal

Quick start #

import { ModalStyled } from "@mshafiqyajid/react-modal/styled";
import "@mshafiqyajid/react-modal/styles.css";

const [open, setOpen] = useState(false);

<button onClick={() => setOpen(true)}>Open</button>

<ModalStyled
  open={open}
  onClose={() => setOpen(false)}
  title="Confirm action"
  footer={
    <>
      <button onClick={() => setOpen(false)}>Cancel</button>
      <button onClick={() => setOpen(false)}>Confirm</button>
    </>
  }
>
  <p>Are you sure you want to proceed?</p>
</ModalStyled>

Drawer variants #

<ModalStyled variant="drawer-right" title="Settings" open={open} onClose={close}>
  <p>Drawer content</p>
</ModalStyled>

<ModalStyled variant="drawer-bottom" title="Options" open={open} onClose={close}>
  <p>Bottom sheet (swipe-to-dismiss enabled automatically)</p>
</ModalStyled>

Confirm variant #

Set confirmVariant="confirm" for a pre-wired confirm/cancel footer. No need to pass footer.

<ModalStyled
  open={open}
  onClose={close}
  title="Delete item"
  confirmVariant="confirm"
  confirmLabel="Delete"
  confirmTone="danger"
  onConfirm={() => { deleteItem(); close(); }}
  onCancel={close}
>
  <p>This action cannot be undone.</p>
</ModalStyled>

Mobile sheet #

Set mobileVariant="sheet" to render as a bottom sheet on viewports ≀ 640 px. Drag-to-dismiss is enabled automatically. Desktop layout is unchanged.

<ModalStyled open={open} onClose={close} title="Options" mobileVariant="sheet">
  <p>Drag down to dismiss on mobile.</p>
</ModalStyled>

Transition #

Choose the panel entrance animation. Only applies to variant="dialog"; drawers always slide. Default: "fade".

<ModalStyled open={open} onClose={close} transition="zoom">...</ModalStyled>
<ModalStyled open={open} onClose={close} transition="slide-up">...</ModalStyled>

Async preventClose guard #

preventClose receives the close reason and can return/resolve true to block the close.

<ModalStyled
  open={open}
  onClose={close}
  preventClose={async (reason) => {
    if (reason === "overlay") return true; // block overlay click
    const dirty = await checkUnsavedChanges();
    return dirty; // true = cancel the close
  }}
>
  <p>Form with unsaved changes</p>
</ModalStyled>

Headless #

import { useModal } from "@mshafiqyajid/react-modal";

const { isOpen, open, close, modalProps, overlayProps } = useModal();

<button onClick={open}>Open</button>

{isOpen && (
  <div {...overlayProps}>
    <div {...modalProps} role="dialog" aria-modal="true">
      <button onClick={close}>Close</button>
      Content
    </div>
  </div>
)}

API #

PropTypeDefaultDescription
openbooleanβ€”Controls visibility. Alias: isOpen (both accepted)
onClose() => voidβ€”Called when modal requests close
titleReactNodeβ€”Header title β€” omit to hide header entirely
descriptionReactNodeβ€”Accessible description rendered below the title; linked via aria-describedby
footerReactNodeβ€”Footer content β€” omit to hide footer. Ignored when confirmVariant="confirm"
size"sm" | "md" | "lg" | "full""md"Dialog width / drawer width
variant"dialog" | "drawer-left" | "drawer-right" | "drawer-bottom""dialog"Layout variant
mobileVariant"default" | "sheet""default"On viewports ≀ 640 px, render as a bottom sheet with drag-to-dismiss
transition"fade" | "zoom" | "slide-up" | "slide-down""fade"Panel entrance animation (dialog variant only; drawers always slide)
blur"none" | "sm" | "md" | "lg""md"Backdrop blur intensity
padding"none" | "sm" | "md" | "lg""md"Body padding
scrollablebooleantrueAllow body to scroll when content overflows
overlayColorstringβ€”Custom backdrop color e.g. "rgba(0,0,0,0.6)"
closeOnOverlayClickbooleantrueClose when clicking the backdrop
closeOnEscbooleantrueClose on Escape key
showCloseButtonbooleantrueShow the Γ— button in the header
lockBodyScrollbooleantrueLock body scroll while open
swipeToDismissbooleanfalse†Allow swipe-down-to-dismiss on touch devices. Defaults to true for drawer-bottom
closeOnSubmitbooleanfalseAuto-close when a contained <form> submits (and default is not prevented)
preventClose(reason: CloseReason) => boolean | Promise<boolean>β€”Return/resolve true to cancel the close
onAfterOpen() => voidβ€”Fires after the open transition completes
onAfterClose() => voidβ€”Fires after the close transition completes
initialFocusRefRefObject<HTMLElement>β€”Element to focus when modal opens
finalFocusRefRefObject<HTMLElement>β€”Element to focus when modal closes
containerHTMLElement | nulldocument.bodyOverride the portal container
confirmVariant"default" | "confirm""default"Set to "confirm" to render a pre-wired confirm/cancel footer
confirmLabelReactNode"Confirm"Confirm button label
cancelLabelReactNode"Cancel"Cancel button label
confirmTone"neutral" | "danger""neutral"Confirm button tone
onConfirm() => voidβ€”Called when confirm button is clicked
onCancel() => voidβ€”Called when cancel button is clicked
styleCSSPropertiesβ€”Inline style override on the panel

CloseReason values: "esc" | "overlay" | "close-button" | "programmatic" | "swipe" | "submit".

Edit this page on GitHub