# JBModal

[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/jb-modal)
[![GitHub license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://raw.githubusercontent.com/javadbat/jb-modal/main/LICENSE)
[![NPM Version](https://img.shields.io/npm/v/jb-modal-react)](https://www.npmjs.com/package/jb-modal-react)
![GitHub Created At](https://img.shields.io/github/created-at/javadbat/jb-modal)

React wrapper for [`jb-modal`](https://www.npmjs.com/package/jb-modal).

## Demo

[Try the interactive modal examples](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--overview), read the [React API docs](https://javadbat.github.io/design-system/?path=/docs/components-jbmodal-react-readme--docs), or [open the GitHub Pages demo](https://javadbat.github.io/jb-modal).

## Installation

```sh
npm i jb-modal
```

```jsx
import { JBModal } from 'jb-modal/react';

<JBModal>Modal content</JBModal>
```

## When to use

Use `JBModal` for temporary blocking UI such as confirmations, forms, detail views, and mobile bottom sheets in React. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--normal)

## Props

| prop | type | description |
| --- | --- | --- |
| `isOpen` | `boolean` | Opens or closes the modal by calling the underlying `open()`/`close()` methods. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--action-test) |
| `id` | `string` | Modal id used for URL hash state. Forwarded to the web component. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--hash-id-and-auto-close) |
| `label` | `string` | Accessible name announced for the dialog. Header slot text is used as a fallback. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--accessibility-behavior) |
| `description` | `string` | Optional accessible description announced after the dialog name. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--accessibility-behavior) |
| `autoCloseOnBackgroundClick` | `boolean` | Automatically closes after an uncanceled backdrop close request. Defaults to `false`. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--close-detail) |
| `autoCloseOnEscape` | `boolean` | Automatically closes after an uncanceled Escape close request. Defaults to `true`. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--accessibility-behavior) |
| `onClose` | `(event) => void` | Fired for backdrop, Escape, and browser-back close requests. Read `event.detail.eventType`; call `preventDefault()` to reject the request. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--cancelable-close-request) |
| `onUrlOpen` | `(event) => void` | Fired when the modal opens itself because the current URL hash matches its id. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--hash-id-and-auto-close) |
| `onLoad` | `(event) => void` | Fired before initialization. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--lifecycle-events) |
| `onInit` | `(event) => void` | Fired after initialization. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--lifecycle-events) |

## Controlled isOpen state

The controlled-state interaction is covered by the [action demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--action-test).

```jsx
const [isOpen, setIsOpen] = useState(false);

<JBModal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
>
  <div slot="content">Modal content</div>
</JBModal>
```

The web component dispatches `close` when the user clicks the background, presses Escape, or triggers browser back while `isOpen` is true. Keep React state in sync in `onClose`.

## isOpen and close

Use `isOpen` as controlled React state. Use `onClose` to update that state when the underlying modal requests close from background click or browser history. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--action-test)

## Slots

The [header and footer demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--with-header-and-footer) shows the `header`, `content`, and `footer` slots.

```jsx
<JBModal isOpen={isOpen} label="Review changes">
  <div slot="header">Modal header</div>
  <div slot="content">Modal content</div>
  <div slot="footer">
    <button onClick={() => setIsOpen(false)}>Close</button>
  </div>
</JBModal>
```

## URL hash state

Set `id` when the modal should update the URL hash while `isOpen` is true. Use `onUrlOpen` to sync React state when the page loads with the modal hash. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--hash-id-and-auto-close)

```jsx
const [isModalOpen, setModalOpen] = useState(false);

<JBModal
  id="profile-modal"
  isOpen={isModalOpen}
  onUrlOpen={() => setModalOpen(true)}
  onClose={() => setModalOpen(false)}
>
  <div slot="content">Profile</div>
</JBModal>
```

## Background click

Background clicks dispatch the underlying `close` event. Set `autoCloseOnBackgroundClick` to close automatically after an uncanceled request. Read `event.detail.eventType` in `onClose` when your app needs to distinguish background clicks from URL-history close requests. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--close-detail)

```jsx
<JBModal isOpen={isOpen} autoCloseOnBackgroundClick onClose={() => setIsOpen(false)}>
  Modal content
</JBModal>
```

Escape closes the web component by default and reports `ESCAPE_KEY`. If a form must remain open, prevent the close request:

```jsx
<JBModal
  isOpen={isOpen}
  label="Edit profile"
  onClose={(event) => {
    if (hasUnsavedChanges) event.preventDefault();
    else setIsOpen(false);
  }}
>
  <ProfileForm />
</JBModal>
```

## Imperative access

Use a ref for imperative `open()` and `close()` calls. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--action-test)

```jsx
const modalRef = useRef(null);

<JBModal ref={modalRef}>
  <div slot="content">Modal content</div>
</JBModal>;

modalRef.current?.open();
modalRef.current?.close();
```

## Custom style

The React component uses the same CSS variables and parts as the web component. See the shared [web-component styling guidance](../README.md#css-parts-and-custom-style) and [style gallery](https://javadbat.github.io/design-system/?path=/story/components-jbmodal-style--gallery).

| CSS variable name | description |
| --- | --- |
| `--jb-modal-bg-color` | Modal content background color. |
| `--jb-modal-back-bg-color` | Modal backdrop background color. |
| `--jb-modal-border-radius` | Modal content border radius. |
| `--jb-modal-corner-shape` | Modal content corner shape; defaults to the global `--jb-corner-shape` token. |
| `--jb-modal-z-index` | Modal z-index. |

```css
.profile-modal::part(content) {
  min-width: 20rem;
}

.profile-modal {
  --jb-modal-border-radius: 1rem;
}
```

```jsx
<JBModal className="profile-modal" />
```

## CSS parts and custom style

The wrapper exposes the same CSS parts and variables as the web component, including content and background parts used by the [style gallery](https://javadbat.github.io/design-system/?path=/story/components-jbmodal-style--gallery).

## Animation

`JBModal` does not provide a default desktop open or close animation. Different products usually need different motion styles, durations, and easing, so the wrapper leaves animation to your project CSS.

Use `className` on `JBModal` and animate the exposed web-component parts. You can animate each part differently: fade the `background`, scale the `content`, slide it from any direction, or give each part its own timing.

```css
@media (min-width: 48.0625rem) {
  .profile-modal::part(background) {
    opacity: 1;
    transition: opacity 300ms ease;
  }

  .profile-modal::part(content) {
    opacity: 1;
    transform: translateY(0) scale(1);
    transition:
      opacity 300ms ease,
      transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
  }

  .profile-modal:state(open)::part(background) {
    @starting-style {
      opacity: 0;
    }
  }

  .profile-modal:state(open)::part(content) {
    @starting-style {
      opacity: 0;
      transform: translateY(1rem) scale(0.96);
    }
  }
}
```

```jsx
<JBModal className="profile-modal" isOpen={isOpen}>
  <div slot="content">Profile</div>
</JBModal>
```

For close animations, add a discrete `display` transition to the modal host and define closed styles for the parts.

See the [animation demo](https://javadbat.github.io/design-system/?path=/docs/components-jbmodal-animation--docs) for open-only and open-close examples.

## Accessibility notes

`JBModal` moves focus into the dialog, traps Tab navigation in the topmost modal, closes on Escape, restores focus to the opener, hides closed content with `inert`, and respects reduced-motion preferences. Provide `label` whenever header text is absent or not descriptive. `description` can supply additional context. [Demo](https://javadbat.github.io/design-system/?path=/story/components-jbmodal--accessibility-behavior)

## Shared Documentation

For web-component behavior, events, slots, CSS parts, URL hash behavior, and the full API, see the shared [`jb-modal` documentation](../README.md).

## AI agent notes

- Import `JBModal` from `jb-modal/react`; the wrapper imports and registers the underlying `jb-modal` web component.
- Use `isOpen` as the controlled React state and update it in `onClose`.
- Use `onUrlOpen` to sync state when the modal opens from a URL hash.
- Use `ref.current.open()` and `ref.current.close()` only for imperative workflows.
- Use `slot="header"`, `slot="content"`, and `slot="footer"` for structured modal content.
- Use a unique `id` for every history-linked modal. For nested modals, browser history records each hash and Back closes only the topmost modal.
