Modal

Accessible modal dialogs for any use case.

zweiundeins <sb-modal> dialog overlay popup confirm Since 2026-09-21 1.8 kB0Open in playground Edit on GitHub

Playground

Are you ready to launch?CancelLaunch

A modal built on the native <dialog>. The browser traps focus, closes on Escape and handles the top layer. Open it with show() and close it with close(), or mark footer buttons with data-sb-close.

Examples

Open from Datastar

data-ref gives you the element as a signal, so any Datastar expression can call its methods.

Launch sequence Are you ready to launch? This cannot be undone. Cancel Launch

<div>
  <sb-button data-on:click="$_launch.show()">Launch sequence</sb-button>
  <sb-modal data-ref:_launch heading="Mission Control" data-on:sb-close="$_answer = evt.detail.value">
    Are you ready to launch? This cannot be undone.
    <sb-button slot="footer" variant="ghost" data-sb-close="cancel">Cancel</sb-button>
    <sb-button slot="footer" data-sb-close="launch">Launch</sb-button>
  </sb-modal>
  <p data-signals:_answer="''" data-show="$_answer" data-text="'You chose: ' + $_answer"></p>
</div>

Inline

The inline attribute renders the panel in place, which is handy for docs and previews.

All systems nominal. Welcome aboard.
<sb-modal inline heading="Docking complete" closable="false">
  All systems nominal. Welcome aboard.
</sb-modal>

Opened by the server

In a CQRS app the server owns "is the dialog open?". Render <sb-modal open> and your SSE stream decides. Datastar's morph keeps the element, and its state lives inside the component.

Accessibility

The native modal <dialog> sets aria-modal, traps focus and restores it on close. The heading labels the dialog. Escape and a click on the backdrop both close it, and sb-close reports why in detail.reason.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-modal inline heading="Mission Control" style="--sb-radius-lg: 8px"><span>Are you ready to launch?</span><sb-button slot="footer" size="sm" variant="outline" data-sb-close>Cancel</sb-button><sb-button slot="footer" size="sm" data-sb-close>Launch</sb-button></sb-modal>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->

<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/modal@54838188ab7b/modal.js" integrity="sha384-MgzZ5v4SAFHE+gfXQnF6Ze4uPBJkAQmL7oy4CVYtiH25zRhvnExueSGbFjm3eagy"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
modal.js 4.9 kB2.2 kB1.8 kB

API reference

Props

AttributeTypeDefaultDescription
headingstring"Dialog"Title of the dialog.
openbooleanfalseOpen. The server opens or closes it by changing the attribute; show() and close() work too.
inlinebooleanfalseRender in place, without an overlay (previews, docs).
closablebooleantrueShow the close button.

Slots

NameDescription
defaultDialog body.
footerAction buttons. Elements with data-sb-close close the dialog.

Events

NameDescription
sb-openAfter opening.
sb-closeAfter closing. detail: { reason, value }.