meowerse_
docs menu

Modal

A dialog over the page that keeps focus inside until it closes.

import { Modal } from "@meowerse/ui"; · source packages/ui/src/components/Modal.tsx

try it

demo source
tsx
import { useId, useState } from "react";
import { Button, Checkbox, Field, Icon, Modal } from "@meowerse/ui";

// A rename dialog whose two hidden controls sit at the trap's EDGES (fix round 1: mid-order hidden
// controls left native Tab to skip them on its own, so a broken usable() never showed). "clear" is
// visibility:hidden and, while empty, the FIRST focusable thing in the panel; the "advanced" section
// is display:none and, while collapsed, the LAST — so wrapping (Shift+Tab from the true first, Tab
// from the true last) only lands correctly if Modal's usable() truly excludes both.
export default function Demo() {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState("");
  const [more, setMore] = useState(false);
  const [saved, setSaved] = useState<string | null>(null);
  const hintId = useId();
  const empty = name.trim() === "";
  const close = () => { setOpen(false); setMore(false); };
  return (
    <div className="demo">
      <Button onClick={() => setOpen(true)}>rename chat</Button>
      {saved !== null && <p role="status" className="demo__out">renamed to “{saved}”</p>}
      <Modal open={open} onClose={close} title="rename chat">
        <div className="demo-modal">
          <div className="demo-modal__row">
            <button type="button" aria-label="clear the name" className={empty ? "demo-modal__clear is-invisible" : "demo-modal__clear"} onClick={() => setName("")}>
              <Icon name="x" />
            </button>
            <Field label="new name" value={name} onChange={(e) => setName(e.target.value)} autoComplete="off" data-autofocus />
          </div>
          {empty && <p id={hintId} className="mw-field__hint">type a name to save.</p>}
          <div className="mw-confirm__actions">
            <Button onClick={close}>cancel</Button>
            <Button variant="primary" disabled={empty} aria-describedby={empty ? hintId : undefined}
              onClick={() => { setSaved(name.trim()); setName(""); close(); }}>save</Button>
          </div>
          <Button variant="ghost" aria-expanded={more} aria-controls="demo-advanced" onClick={() => setMore((m) => !m)}>{more ? "fewer options" : "more options"}</Button>
          <div id="demo-advanced" className={more ? "demo-modal__more" : "demo-modal__more is-collapsed"}>
            <Checkbox label="tell the members" />
          </div>
        </div>
      </Modal>
    </div>
  );
}

props

proptypedefaultrequired
openboolean whether the dialog is open.—yes
onClose() => void called when Escape or the backdrop closes the dialog.—yes
titlestring the dialog's title, used to label it for assistive tech.—yes
childrenReactNode the dialog's content.—yes
classNamestring extra class names to append to the dialog panel.—no

css custom properties

variabledarklight
--c-line-strong#404044#a9a8a2
--c-scrimrgba(0, 0, 0, 0.6)rgba(20, 20, 22, 0.4)
--c-surface-raised#1a1a1d#f7f6f3
--fs-51.25remsame
--r-l8pxsame
--shadow-lg0 16px 40px rgb(0 0 0 / 0.35) (legacy alias)—
--sp-28pxsame
--sp-416pxsame
--sp-524pxsame
--z-sheet30same

Classes: .mw-modal, .mw-modal-backdrop. Set any of these variables on an ancestor (or wrap a subtree in .mw-theme--dark / .mw-theme--light) to restyle just that part of a page.

accessibility

  • role="dialog" with aria-modal, labelled by its title.
  • Focus moves inside on open, cycles only through controls you can use (it skips disabled, hidden, display:none and visibility:hidden ones) and returns to the trigger on close.
  • Escape and the backdrop close it; with two dialogs open, only the top one reacts.
  • It has no close button of its own and doesn't lock page scroll yet (audit U-08): always give it a visible "cancel" or "close" button.
keydoes
Tab / Shift+Tabcycles through the dialog's controls
Escapecloses it

do and don't

do

  • Put the main action last and a cancel button next to it.

don't

  • Don't open a modal on page load.
  • Don't put a whole page inside one.