GitHub

Alert Box

Inline status message with a variant-mapped icon, optional title, and optional dismiss control.

Stable .astro import EsaAlertBox from '@esa/ecology/esa-alert-box.astro';

Preview

Heads up
This is an informational message.
<AlertBox variant="info" title="Heads up">
  This is an informational message.
</AlertBox>

Variants

Heads up
This is an informational message.
Saved
Your changes were saved successfully.
Check this
Some fields need your attention.
Error
Something went wrong while saving.
<AlertBox variant="info" title="Heads up">This is an informational message.</AlertBox>
<AlertBox variant="success" title="Saved">Your changes were saved successfully.</AlertBox>
<AlertBox variant="warning" title="Check this">Some fields need your attention.</AlertBox>
<AlertBox variant="danger" title="Error">Something went wrong while saving.</AlertBox>

States

The title is optional — omit it for a compact single-line alert. With dismissable, a close button appears and a dismissed event bubbles on click.

A compact, single-line alert with just a message.
Dismiss me
Click the × to hide this alert.
<AlertBox variant="info">A compact, single-line alert with just a message.</AlertBox>
<AlertBox variant="warning" title="Dismiss me" dismissable={true}>Click the × to hide this alert.</AlertBox>

API

PropTypeDefaultDescription
variant 'info' | 'success' | 'warning' | 'danger' 'info' Tone — drives the background, border, and auto-mapped icon.
title string — Optional bold heading above the message.
dismissable boolean false Shows a close button that hides the alert and emits a dismissed event. Dismissing moves focus to the next focusable element after the box, rather than dropping it to the top of the document.
live boolean false Announce the box when it is injected into an already-loaded page. Off by default, and deliberately so — see Accessibility below. Ignored (with a console warning) when combined with dismissable, because a live region announces a button as bare text and gives no way to reach it.

Events

EventTypeDefaultDescription
dismissed CustomEvent — Bubbles when the close button is clicked (dismissable only).

Accessibility

It is not a live region, despite the name

An alert box is the component most likely to be injected at runtime, so making it role="alert" by default looks obvious. It is wrong for the general case, for three reasons that compound:

  • It takes a slot. Live regions announce raw text with no structure, so a slotted link or button is read as a run-on sentence with no roles. The component cannot see what you put in there.
  • dismissable puts a control inside it. A live region does not take focus and cannot be navigated to, so the user is told a button exists and given no route to it. A message with a control in it is a dialog — use esa-dialog.
  • Most alert boxes are present at page load, where there is no change for a live region to report — or worse, where some screen readers read it out on load as noise.

So live is opt-in. Turn it on only when all three hold: the box appears in response to something the user did, it is not dismissable, and its slot is plain text. danger then announces assertively (role="alert"); the other variants wait for a pause (role="status"). Combining live with dismissable is ignored and warns in the console.

Everything else

  • Icons are auto-mapped per variant (info / circle-check / triangle-alert / circle-alert) and marked aria-hidden.
  • The dismiss button has an aria-label="Dismiss".
  • Dismissing moves focus to the next focusable element after the box. Hiding the box removes the button that had focus, and without this focus falls to <body> — silently returning a keyboard user to the top of the document.

Theming surface

Every public token this component reads, extracted from its source at build time, split by blast radius: the hooks that belong to this component, then the shared and system tokens it consumes alongside everything else. The Resolves to column walks each token's real lineage — component → semantic → primitive → raw value — so you can see exactly where re-pointing it lands. A spoke re-skins by overriding these in its theme-<slug>.css; component internals are never edited. No hook for what you need? File it with /request-lego.

Type roles it names

This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.

RoleRe-pointReach
.typography-body-sm
  • --typography-body-sm-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-sm-font-size clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150
  • --typography-body-sm-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-sm-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-sm-line-height 1.6 via --line-height-normal
+20 more components esa-breadcrumbs, esa-button-toggle, esa-card, esa-checkbox-group, esa-combobox, esa-command-palette, esa-date-picker, esa-empty-state, esa-file-upload, esa-form-field, esa-input-tag, esa-link-column, esa-loading-overlay, esa-progress-bar, esa-radio-group, esa-search-panel, esa-select, esa-stat, esa-text-field, esa-textarea
.typography-label-sm-strong
  • --typography-label-sm-strong-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-sm-strong-font-size clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150
  • --typography-label-sm-strong-font-weight 550 via --typography-font-weight-semibold → --font-weight-550
  • --typography-label-sm-strong-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-sm-strong-line-height 1.6 via --line-height-normal
+2 more components esa-collapsible, esa-empty-state

Wired to this component

No other component reads these — re-point them and esa-alert-box is the only thing that moves.

Token Tier Resolves to
--alert-box-text-color component #646464 via --color-content-default-secondary → --color-gray-11

Shared tokens it reads

Owned by a family or by the semantic layer. Still overridable — but scope the override (esa-foo { --form-border-color: … }) unless you mean to move everything that reads it. Reach says what else that is.

Token Reach Resolves to
--focus-ring-color component focus ring group · +34 more #3e9b4f via --color-border-default-focus → --color-grass-10
--focus-ring-offset component focus ring group · +31 more 2px
--focus-ring-width component focus ring group · +35 more 2px via --border-width-focus → --border-width-200
--border-width-default semantic every component 1px via --border-width-100
--color-background-overlay-strong-hover semantic every component rgba(0, 0, 0, 0.05) via --color-black-a-1
--color-background-utility-danger-subtle semantic every component #fffcfc via --color-red-1
--color-background-utility-info-subtle semantic every component #fbfdff via --color-blue-1
--color-background-utility-success-subtle semantic every component #fbfefc via --color-green-1
--color-background-utility-warning-subtle semantic every component #fefdfb via --color-yellow-1
--color-border-utility-danger semantic every component #fdbdbe via --color-red-6
--color-border-utility-info semantic every component #acd8fc via --color-blue-6
--color-border-utility-success semantic every component #adddc0 via --color-green-6
--color-border-utility-warning semantic every component #f3d673 via --color-yellow-6
--color-content-default semantic every component #202020 via --color-gray-12
--color-content-default-secondary semantic every component #646464 via --color-gray-11
--color-content-utility-danger semantic every component #ce2c31 via --color-red-11
--color-content-utility-info semantic every component #0d74ce via --color-blue-11
--color-content-utility-success semantic every component #218358 via --color-green-11
--color-content-utility-warning semantic every component #ab6400 via --color-yellow-11
--radius-md semantic every component 0.5rem via --radius-200
--radius-sm semantic every component 0.25rem via --radius-100
--transition-fast semantic every component 150ms via --duration-150
--spacing-050 primitive every component 0.125rem
--spacing-300 primitive every component 0.75rem
--spacing-400 primitive every component 1rem