---
title: "JavaScript Modal"
description: "The LemonadeJS Modal block for JavaScript: Draggable, resizable window overlay; the primitive under the other popups. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/modal/
---

<link rel="stylesheet" href="/v6/modal.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">

# JavaScript Modal

`@lemonadejs/modal` · ✓ 63 contract checks · framework-agnostic · zero dependencies

`<Modal />` — the platform primitive. Floating panels, dropdown lists,
autocomplete, corner chats and the context menu are all built on these
behaviors, ported faithfully from v5:

  - resize from all 8 edges/corners (10px hit zone) with live cursor
    feedback; Shift preserves the aspect ratio
  - drag by the top 40px zone with a move cursor — improved over v5:
    the grab zone is CLAMPED to the viewport, a modal can never be
    dragged irrecoverably off-screen
  - minimize DOCKS to a taskbar row at the bottom of the screen
    (205px slots, wrapping), restore returns to the remembered spot
  - explicit coordinates on open (centered unless positioned), margin
    based auto-adjust, responsive fullscreen on small screens; flip
    mode for anchored panels (dropdowns) inverts above the anchor at
    the bottom edge instead of covering it, api.adjust() re-anchors
    after content changes the panel size while open
  - Escape/focus handling scoped to the ELEMENT (multiple modals never
    fight over a document listener), v5 close origins preserved

v5 → v6 mapping: closed → bind (inverted: bind is the OPEN state);
auto-close → autoclose; auto-adjust → autoadjust; content → children.
position: 'absolute' is CSS-anchored exactly like v5 (the host's
positioned ancestor places it — dropdown panels); 'fixed' takes
explicit viewport coordinates (context menus at the cursor).
onclose(origin): 'button' | 'backdrop' | 'escape' | 'focusout' | 'api'.
onmove(top, left) and onresize(width, height) fire on release.

## Example

<!--example-->

```js
import { html, ref } from 'lemonadejs';
import Modal from '@lemonadejs/modal';

const App = (props, { state }) => {
    const modal = ref();
    const closedVia = state('');

    return html`<div>
        <button onclick="${() => modal.current.open()}">Open project settings</button>
        <p>Last closed via: <b>${() => closedVia.value || '(not yet)'}</b></p>

        <${Modal} ref="${modal}" title="Project settings" width="420" height="240"
            backdrop closable draggable resizable minimizable
            onclose="${(origin) => (closedVia.value = origin)}">
            <p style="margin:0 0 8px">Drag the header, resize from any edge or corner,
            minimize to the dock at the bottom of the screen.</p>
            <p style="margin:0">Close with the ×, a backdrop click or Escape.</p>
        </${Modal}>
    </div>`;
};
```

## Installation

```bash
npm install @lemonadejs/modal
```

```js
import Modal from '@lemonadejs/modal';
import '@lemonadejs/modal/style.css';
```

The icons come from Google Material Symbols. Load the font once per page:

```html
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
```

Three deployment forms, one component:

```js
html`<${Modal} />`                       // by value (no registration)
setComponents({ Modal });               // then <Modal /> by name anywhere
createWebComponent(Modal);              // <lm-modal> in plain HTML/any framework
```

## Props

Every declared prop arrives as a **live state** — pass a value for a snapshot or a
state for a two-way live wire. Attribute strings are coerced to the declared type.

| Prop | Type | Default | Description |
|---|---|---|---|
| `bind` | boolean | — | Two-way bound value. `.set()` fires `onchange`; plain assignment is silent. open state (v5: closed, inverted) |
| `title` | string | `''` |  |
| `width` | number | `0` |  |
| `height` | number | `0` |  |
| `top` | number | `0` |  |
| `left` | number | `0` |  |
| `position` | string | `''` | center | left | right | bottom | fixed (explicit viewport |
| `backdrop` | boolean | `false` |  |
| `closable` | boolean | `false` |  |
| `draggable` | boolean | `false` |  |
| `resizable` | boolean | `false` |  |
| `minimizable` | boolean | `false` |  |
| `minimized` | boolean | `false` |  |
| `fullscreen` | boolean | `false` | cover the whole viewport |
| `header` | boolean | `true` | false: headerless floating panel (menus, chips) |
| `role` | string | `''` | ARIA role: '' = auto (backdrop → dialog, else none) |
| `label` | string | `''` | accessible name fallback when there is no title (aria-label) |
| `describedby` | string | `''` | aria-describedby id passthrough (dialog message wiring) |
| `autoclose` | boolean | `false` | v5: auto-close |
| `autoadjust` | boolean | `false` | v5: auto-adjust |
| `flip` | number | `0` | anchored panels: at the bottom edge, flip ABOVE the natural top, |
| `icon` | string | `''` | material icon name, shown before the title |
| `radius` | boolean | `true` | rounded corners (false: square — anchored panels) |
| `focus` | boolean | `true` |  |
| `outline` | boolean | `false` | paint the catalog focus ring on the panel itself |
| `overflow` | boolean | `false` |  |
| `responsive` | boolean | `true` |  |
| `layers` | boolean | `false` |  |
| `url` | string | `''` |  |

## Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

- `onopen`
- `onclose`
- `onmove`
- `onresize`

## API

```js
import { ref } from 'lemonadejs';
const modal = ref();
html`<${Modal} ref="${modal}" />`;
// modal.current.open(...)  ·  modal.current.close(...)  ·  modal.current.toggle(...)  ·  modal.current.front(...)  ·  modal.current.back(...)  ·  modal.current.adjust(...)
```

- `open()`
- `close()`
- `toggle()`
- `front()`
- `back()`
- `adjust()`

## Styling

All classes follow the `lm-modal-*` convention; visual variants are `data-*`
attributes on the root. Override freely — there is no styling engine to fight.

## Contract

The machine-readable schema ships with the package:

```js
import contract from '@lemonadejs/modal/contract.json';
```

`verify.json` carries the conformance proof produced by `verify(Modal)`.

Looking for the v5 plugin? See the [archived v5 documentation](/docs/v5/plugins/).