Alert

Contextual feedback messages for your app.

zweiundeins <sb-alert> notice callout status message Since 2026-09-21 MIT licence 1.4 kB0 Open in playground Edit on GitHub

Playground

Your component is live.

Alerts tell people what just happened. A pixel status light, a tinted surface and an optional dismiss button, in four tones.

Examples

Variants

Telemetry resumes in 30 seconds. Your component is live. Consider a gravity assist. Seal deck 7 immediately.
<div style="display: grid; gap: 12px; inline-size: 100%">
  <sb-alert heading="Heads up">Telemetry resumes in 30 seconds.</sb-alert>
  <sb-alert variant="success" heading="Launch successful!">Your component is live.</sb-alert>
  <sb-alert variant="warning" heading="Low fuel">Consider a gravity assist.</sb-alert>
  <sb-alert variant="danger" heading="Hull breach">Seal deck 7 immediately.</sb-alert>
</div>

Dismissible

The close button hides the alert and emits sb-close. From script, host.open reads and sets whether it is shown, and show() / hide() do the same; none of them writes the open attribute.

Check the gallery for fresh arrivals. show()
<div style="display: grid; gap: 8px; inline-size: 100%">
  <sb-alert data-ref:_news variant="info" closable heading="New components landed">Check the gallery for fresh arrivals.</sb-alert>
  <sb-button size="sm" variant="outline" style="justify-self: start" data-on:click="$_news.show()">show()</sb-button>
</div>

Server-driven

With Datastar the backend decides when to show an alert: patch it into the page from an SSE response.

<div id="flash">
  <sb-alert id="flash-42" variant="success" closable heading="Saved">Mission plan stored.</sb-alert>
</div>

The open attribute is the server's:

The server sends What happens
a changed open attribute It wins, over a dismissal too: open="false" hides the alert; open, open="true" or no attribute shows it again
the same markup again Nothing. A dismissed alert stays dismissed, even with a new message in it

So a new message needs a new element: give each message its own id (as above), or patch it in with mode replace. A dismissal stays in the browser: sb-close (a plain event, no detail) fires for the close button only, not for hide(), host.open or the server, so listen to it if the server should know.

Styling

Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.

  • Fonts: the heading and the message use your page's font.
  • Colours: each variant has a tone: --sb-info, --sb-ok (success), --sb-warn (warning) or --sb-danger. It colours the status light and the heading (unless the page's color-scheme is dark, the heading mixes it with --sb-text-1, for contrast), and tints the border and the background. The box is --sb-surface-inset with a --sb-border edge, the message --sb-text-2 (the close button turns --sb-text-1 on hover), the corners --sb-radius.
  • Status light: a pixel shape notched by --sb-notch, a dot at 0. The icon slot replaces it, e.g. with an icon per variant so the tone isn't told by colour alone; give your icon aria-hidden="true" when the text names the severity. In forced colours (Windows High Contrast) the light is drawn in CanvasText.
  • Parts: alert (the box), heading and message. Your page's ::part() rules win over the component's own, without !important.
  • State: a closed alert (dismissed, open="false" or hide()) matches sb-alert:state(closed) and takes no space in the layout, like one with the hidden attribute.
Bay 3 opens at 14:00. Consider a gravity assist.
<style>
  .my-alert { --sb-info: var(--sb-brand-light); }
  .my-alert::part(alert) { border-radius: 0; padding: 1rem 1.25rem; }
  .my-alert::part(message) { font-size: 1rem; }
</style>
<div style="display: grid; gap: 12px; inline-size: 100%">
  <sb-alert class="my-alert" heading="Docking window">Bay 3 opens at 14:00.</sb-alert>
  <sb-alert variant="warning" heading="Low fuel"><span slot="icon" aria-hidden="true">⛽</span>Consider a gravity assist.</sb-alert>
</div>

Accessibility

Warning and danger alerts use role="alert" and are announced as soon as they appear. Info and success use the polite role="status", which screen readers announce reliably only when the region was on the page before its text arrived: one patched in together with its message may go unannounced. When an info or success message must be heard, patch it into a container that stays on the page and carries role="status" itself (<div id="flash" role="status">).

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 a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.

<script type="importmap">
  { "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.

Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/alert@311fc7f0f135/alert.bundle.min.js" integrity="sha384-6oCOMN0rnFB+uBqO3TdiKuT1Kd4b7XK7Zztz9Yk5xa+iyLwdbBt8mM7KlMs+Xc0y"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/alert@311fc7f0f135/alert.min.js": "sha384-HJMmGRKN4kMZChk/5jVLV5kDDKOhaLmCpRlLHzXrywtP93B9727jQrrvnpaINl4z"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/alert@311fc7f0f135/alert.min.js" integrity="sha384-HJMmGRKN4kMZChk/5jVLV5kDDKOhaLmCpRlLHzXrywtP93B9727jQrrvnpaINl4z"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/alert@311fc7f0f135/alert.bundle.min.js": "sha384-6oCOMN0rnFB+uBqO3TdiKuT1Kd4b7XK7Zztz9Yk5xa+iyLwdbBt8mM7KlMs+Xc0y"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).

  • <sb-alert>
    • alert.bundle.min.js one file 1.4 kB brotli
<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/alert/alert.bundle.min.js"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

Or copy the module files, keeping each component's folder, and load each component's module:

<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/alert/alert.min.js"></script>

<sb-alert variant="success" heading="Saved" closable>Your changes are live.</sb-alert>

Size

A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
alert.bundle.min.js 3.3 kB1.6 kB1.4 kB
As module files: one request, 1.4 kB minified with brotli

For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.

FileOriginalgzipbrotliminified
alert.js 6.1 kB2.7 kB2.3 kB1.4 kB

API reference

Props

AttributeTypeDefaultDescription
variant"info" | "success" | "warning" | "danger""info"Tone of the message.
headingstring""Bold first line.
closablebooleanfalseShow a dismiss button.
openbooleantrueShown. Any change the server makes to the attribute wins, over a dismissal too: open="false" hides it, open or no attribute shows it. Unchanged markup keeps a dismissal. From script: host.open, show(), hide().

Slots

NameDescription
defaultThe message.
iconReplaces the status light, e.g. with an icon per variant. Mark it aria-hidden="true" when the text names the severity.

Events

NameDescription
sb-closeThe user pressed the close button (not sent for hide(), host.open or the server). No detail.