jb-modal
v1.15.0
Published
modal web component
Maintainers
Readme
jb-modal
Responsive modal web component.
Benefits:
- Responsive, opens as a centered dialog on desktop and a bottom sheet on mobile devices.
- Framework free, so you can use it anywhere.
- Customizable content and style.
- Pre-styled header and footer slots.
- TypeScript support.
- Optional auto close on background click.
- Optional route history with browser back handling.
- Keeps modal open after page refresh when you provide an
id.
When to use
Use jb-modal for temporary blocking UI such as confirmations, forms, detail views, and mobile bottom sheets. See the basic modal demo.
Use an inline panel or page route when the content should remain part of the normal document flow or needs a shareable full-page URL.
Demo
Try the interactive modal examples or open the GitHub Pages demo.
Using With JS Frameworks
See the React API and examples.
Other integrations: Angular · Vue · Nuxt · Svelte · SvelteKit · SolidJS · Lit · Next.js · Astro · Blazor · Server-rendered templates · WordPress · Alpine.js and HTMX
Installation
npm i jb-modalimport 'jb-modal';<jb-modal>
<div>Modal content</div>
</jb-modal>API reference
Attributes
| name | type | default | description |
| --- | --- | --- | --- |
| is-open | boolean | false | Opens the modal when set to "true". Any other value closes it. Demo |
| id | string | "" | Modal id used for URL hash state. Demo |
| label | string | header text or localized "Dialog" | Accessible name announced by assistive technology. Use this when the visible header does not provide a useful name. Demo |
| description | string | "" | Optional accessible description announced after the modal name. Demo |
Properties
| name | type | readonly | description |
| --- | --- | --- | --- |
| isOpen | boolean | yes | Current isOpen state. Demo |
| id | string | no | Modal id used for URL hash state. See the hash-state demo. |
| JBID | symbol | yes | Internal unique symbol for this modal instance. |
| autoCloseOnBackgroundClick | boolean | no | Automatically closes after an uncanceled backdrop close request. Defaults to false. Demo |
| autoCloseOnEscape | boolean | no | Automatically closes after an uncanceled Escape close request. Defaults to true. Demo |
Methods
| name | returns | description |
| --- | --- | --- |
| open() | void | Opens the modal, moves focus into it, and pushes #id to browser history when id is set. Demo |
| close() | void | Closes the modal, restores focus to its opener, and navigates back when it owns the current hash entry. Demo |
Events
The lifecycle story demonstrates load and init; close and URL events are covered by the linked interaction examples. Demo
| event | detail | description |
| --- | --- | --- |
| load | none | Dispatched from connectedCallback before initialization. |
| init | none | Dispatched from connectedCallback after initialization. |
| urlOpen | none | Dispatched when the modal opens itself because the current URL hash matches its id. Demo |
| close | { eventType } | Cancelable, bubbling event dispatched for user close requests. Call preventDefault() to reject the request. Demo |
close event event.detail.eventType can be:
| value | meaning |
| --- | --- |
| BACKGROUND_CLICK | The backdrop was clicked. |
| HISTORY_BACK_EVENT | Browser back/popstate was received while the modal was open. |
| CLOSE_BUTTON_CLICK | Reserved close type for close button flows. |
| ESCAPE_KEY | The user pressed Escape while this was the topmost modal. |
Programmatic calls to close() do not dispatch a close event.
isOpen and close
The open/close methods and controlled isOpen state are exercised in the action demo.
const modal = document.querySelector('jb-modal');
modal.open();
modal.close();
console.log(modal.isOpen);<jb-modal is-open="true">
<div>Modal content</div>
</jb-modal>Slots
See the header and footer slot demo and the slot documentation.
Use the default slot for simple content or named slots for structured modal sections.
<jb-modal>
<div slot="header">Modal header</div>
<div slot="content">Modal content</div>
<div slot="footer">
<jb-button>Done</jb-button>
</div>
</jb-modal>| slot | description |
| --- | --- |
| default | Modal content rendered as the fallback of the content slot. |
| header | Header area at the top of the content box. |
| content | Main scrollable modal content area. |
| footer | Footer area at the bottom of the content box. |
Background click
The close-detail demo shows the emitted BACKGROUND_CLICK detail and auto-close behavior.
The component always dispatches close with eventType: "BACKGROUND_CLICK" when the backdrop is clicked. It only closes automatically when autoCloseOnBackgroundClick is true.
const modal = document.querySelector('jb-modal');
modal.autoCloseOnBackgroundClick = true;
modal.addEventListener('close', (event) => {
if (event.detail.eventType === 'BACKGROUND_CLICK') {
console.log('Backdrop clicked');
}
});URL hash state
Set id when the modal should update the URL hash while isOpen is true. When open() runs, the modal pushes #id to browser history. If the page loads with the same hash, the modal opens itself and dispatches urlOpen.
Try the hash-state demo. To test the real browser hash and back-button behavior, open the isolated hash demo in a new window.
<jb-modal id="profile-modal">
<div slot="content">Profile</div>
</jb-modal>const modal = document.querySelector('#profile-modal');
modal.addEventListener('urlOpen', () => {
console.log('Opened from URL hash');
});Browser back dispatches close with eventType: "HISTORY_BACK_EVENT" and closes the topmost matching modal. This is independent of autoCloseOnBackgroundClick. Preventing the event restores that modal's history entry so the visible modal and URL remain synchronized.
Multiple and nested modals
Open modals are recorded by JBModalManager. Only the topmost modal traps focus, handles Escape, responds to browser history, and restores focus. Closing a child modal makes its parent topmost again and returns focus to the control that opened the child.
Give every history-linked modal a unique id. The hash represents only the current topmost modal, while browser history preserves the stack:
/page -> #account-modal -> #delete-confirmation-modalOpening the confirmation from the account modal pushes #delete-confirmation-modal. Browser Back closes only the confirmation and restores #account-modal; another Back closes the account modal.
A fresh page load at #delete-confirmation-modal can open that modal, but a single hash cannot reconstruct its parent modal stack. If the child requires parent context, the application must restore that context itself before opening the child.
CSS parts and custom style
For complete styling guidance, live examples, and copyable style recipes, see the Styling guide and style gallery.
| part | description |
| --- | --- |
| background | The modal backdrop/background. |
| content-box | The modal content box that contains header, content, and footer slots. |
| component-wrapper | div that wrap whole component |
| 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-z-index | Modal z-index. |
jb-modal::part(content-box) {
min-width: 20rem;
}
jb-modal {
--jb-modal-border-radius: 1rem;
--jb-modal-z-index: 1000;
}Animation
jb-modal does not ship with a default desktop open or close animation. Modal animation is usually tied to each project's visual language, motion duration, easing, and interaction style, so the component keeps the behavior simple and lets you add animation from your own CSS.
You can animate each exposed part independently. For example, fade the background, scale or slide the content-box, or use different durations for each part.
@media (min-width: 48.0625rem) {
.profile-modal::part(background) {
opacity: 1;
transition: opacity 300ms ease;
}
.profile-modal::part(content-box) {
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-box) {
@starting-style {
opacity: 0;
transform: translateY(1rem) scale(0.96);
}
}
}For close animations, include a discrete display transition on the modal host so the element remains rendered while its parts animate back to their closed styles.
See the animation demo for open-only and open-close examples.
Accessibility notes
- The component exposes
role="dialog"andaria-modal="true"throughElementInternals. Accessibility demo - Set
labelto provide a stable accessible name. Without it, the component uses text from theheaderslot, then the localized word “Dialog”. descriptionprovides an optional accessible description.- Opening focuses
[autofocus], otherwise the first focusable control, otherwise the modal content container. - Tab and Shift+Tab stay inside the topmost modal. Escape closes the topmost modal by default.
- Closing restores focus to the element that opened the modal when that element still exists and can receive focus.
- Closed modal content is
inert, preventing keyboard and assistive-technology access. - The built-in mobile animation is disabled when the user requests reduced motion.
<button id="open-settings">Open settings</button>
<jb-modal label="Account settings" description="Update your profile and notification preferences">
<form slot="content">
<input autofocus aria-label="Display name" />
</form>
</jb-modal>Related Docs
- See
jb-modal/reactif you want to use this component in React. - See All JB Design System Component List for more components.
- Use Contribution Guide if you want to contribute to this component.
AI agent notes
- Import
jb-modalonce before using<jb-modal>. - Use
open()andclose()for imperative control; there is no publicopenproperty setter. - Use
is-open="true"only for initialisOpenmarkup state. - Use
autoCloseOnBackgroundClick = truewhen backdrop clicks should close the modal. - Listen to
closeand inspectevent.detail.eventTypeto know why close was requested. - Escape closes automatically by default. Set
autoCloseOnEscape = falseto emit only the close request. - Call
event.preventDefault()from acloselistener when validation or unsaved work must keep the modal open. - Use
idonly when URL hash/history integration is desired. - This package includes
custom-elements.jsonand points to it with the package.jsoncustomElementsfield. The field is documented by the Custom Elements Manifest project in Referencing manifests from npm packages. - In
custom-elements.json,exports.kind: "js"describes JavaScript/TypeScript exports andexports.kind: "custom-element-definition"maps thejb-modaltag name toJBModalWebComponent.
