Limited offer: $250 off for the first 200 customers!Claim discount

Modal

An accessible dialog built on the native HTML dialog element — closes on Escape, backdrop click, or the × button.

What it does

Modal wraps the browser's native <dialog> element. It uses showModal() and close() directly, which means focus is trapped inside automatically, Escape closes it, and the backdrop is handled by the browser — no extra libraries needed.

The modal syncs open/closed state via the isOpen prop. Your parent component owns the state and passes the setter down as setIsOpen.

Props

  • isOpen — boolean. When true the dialog opens; when false it closes. Required.
  • setIsOpen — a (open: boolean) => void setter. The modal calls it with false when the user dismisses — via Escape, the backdrop, or the × button. Required.
  • title — optional string rendered as a heading inside the modal. Leave it out if you want to control the heading yourself via children.
  • children — any JSX rendered inside the modal body. Required.

Usage

"use client";
import { useState } from "react";
import Modal from "@/components/Modal";

export default function Example() {
  const [open, setOpen] = useState(false);

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

      <Modal isOpen={open} setIsOpen={setOpen} title="Confirm action">
        <p>Are you sure you want to continue?</p>
        <div className="mt-4 flex justify-end gap-2">
          <button onClick={() => setOpen(false)}>Cancel</button>
          <button onClick={handleConfirm}>Confirm</button>
        </div>
      </Modal>
    </>
  );
}

Styling

The dialog is constrained to max-w-lg and centered with m-auto. To change the width, wrap the modal content in a <div> with a custom width class, or override the dialog size directly in src/components/Modal.tsx. The backdrop uses a semi-transparent neutral overlay — adjust the backdrop:bg-neutral/50 class on the <dialog> element to change it.

TipKeep modal content short and focused on a single decision or form. If the content requires scrolling, it should probably be a separate page instead.
TipAlways give the user an obvious way out — the × button and Escape are handled for you, but also include a "Cancel" button inside the content for discoverability. Users who don't know about Escape will look for a button first.