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

live
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

npm install @lemonadejs/modal
import Modal from '@lemonadejs/modal';
import '@lemonadejs/modal/style.css';

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

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

Three deployment forms, one component:

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.

PropTypeDefaultDescription
bindbooleanTwo-way bound value. .set() fires onchange; plain assignment is silent. open state (v5: closed, inverted)
titlestring''
widthnumber0
heightnumber0
topnumber0
leftnumber0
positionstring''center
backdropbooleanfalse
closablebooleanfalse
draggablebooleanfalse
resizablebooleanfalse
minimizablebooleanfalse
minimizedbooleanfalse
fullscreenbooleanfalsecover the whole viewport
headerbooleantruefalse: headerless floating panel (menus, chips)
rolestring''ARIA role: ” = auto (backdrop → dialog, else none)
labelstring''accessible name fallback when there is no title (aria-label)
describedbystring''aria-describedby id passthrough (dialog message wiring)
autoclosebooleanfalsev5: auto-close
autoadjustbooleanfalsev5: auto-adjust
flipnumber0anchored panels: at the bottom edge, flip ABOVE the natural top,
iconstring''material icon name, shown before the title
radiusbooleantruerounded corners (false: square — anchored panels)
focusbooleantrue
outlinebooleanfalsepaint the catalog focus ring on the panel itself
overflowbooleanfalse
responsivebooleantrue
layersbooleanfalse
urlstring''

Events

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

  • onopen
  • onclose
  • onmove
  • onresize

API

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:

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.