AlertBanner

Inline status banner for common tones.

Live demo

01
Heads up
A scheduled maintenance window starts at 02:00 GMT.
Saved!
Your changes are live.
A new version is available

Implementation

02
AlertBanner.svelte
<script lang="ts">
  import AlertBanner from '$lib/components/AlertBanner.svelte';
  let shown = $state(true);
</script>
​
{#if shown}
  <AlertBanner
    variant="success"
    title="Saved"
    message="Your changes are live."
    dismissable
    onDismiss={() => (shown = false)}
  />
{/if}

AlertBanner is an inline status banner β€” it lives in the page flow, not in a toast layer. Each variant ships its own colour palette and inline SVG icon. The component picks the right ARIA role automatically (assertive role='alert' for warning/error, polite role='status' for info/success), so screen readers announce critical issues immediately and informational notes politely. Pass dismissable to add an Γ— button, and a children snippet to nest links or buttons inside the banner.

Logic explainer

03

What Does It Do? (Plain English)

AlertBanner is an inline status notice that lives in the document flow and stays put until something on the page removes it. Use it to communicate state directly where the user is reading β€” a save-failed message under a form, a "trial ends in 3 days" banner above a dashboard, an empty-results note inside a filtered list. Each banner picks one of four variants β€” info, success, warning, error β€” and adopts both the right colour palette and the right ARIA role automatically.

Think of it as a polite Post-it note stuck to the page: visible, dismissible if you want it to be, but never floating over the rest of the UI the way a Toast does.

How It Works (Pseudo-Code)

props:
  variant      = 'info' | 'success' | 'warning' | 'error'  // default 'info'
  title        = optional bold heading
  message      = optional body line
  dismissable  = boolean                                    // default false
  onDismiss    = optional callback
  children     = optional snippet (action buttons / links)

derive role:
  if variant is 'error' or 'warning': role = 'alert'        // assertive
  else:                                role = 'status'      // polite
  aria-live = role === 'alert' ? 'assertive' : 'polite'

render:
  <div role={role} aria-live={...}>
    <icon for {variant} />              // inline SVG, aria-hidden
    <body>
      {if title}    <strong>{title}</strong>
      {if message}  <p>{message}</p>
      {if children} <div class="actions">{render children}</div>
    </body>
    {if dismissable}
      <button aria-label="Dismiss" onclick={() => onDismiss?.()}>Γ—</button>
  </div>

The Core Concept: Variant β†’ Both Colour And Role

Most banner libraries treat appearance and accessibility as two separate decisions: pass colour="red" and separately set role="alert". AlertBanner ties them together because they're really the same decision in disguise. The variant is the intent, and intent fixes both the visual treatment and how assistive tech should announce it.

variant     palette                 ARIA role     aria-live
─────────────────────────────────────────────────────────────
info        blue tint               status        polite
success     green tint              status        polite
warning     amber tint              alert         assertive
error       red tint                alert         assertive

Why this matters: an error banner that's only styled red but uses role="status" fails silently for blind users β€” the screen reader doesn't interrupt to announce it, and they may discover the failure several minutes later when they Tab past it. Conversely, a polite "Saved!" banner with role="alert" rudely interrupts whatever the user was reading. Tying role to variant means the visual designer can pick variant="error" and the right ARIA semantics fall into place automatically.

The component implements this with a tiny $derived:

let role = $derived(variant === 'error' || variant === 'warning' ? 'alert' : 'status');

That's it. One line, one source of truth.

CSS Animation Strategy

The banner mounts with a 200ms slide-in:

@keyframes slide-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

transform and opacity are GPU-accelerated and don't trigger layout, so the animation stays smooth even if the banner appears in the middle of a long page. The motion is intentionally small β€” four pixels β€” because the banner is part of the page layout, not a floating panel. A larger entrance would imply "this is a separate UI layer" and is wrong for an inline notice.

prefers-reduced-motion: reduce removes the animation outright. Because the banner participates in document flow, it doesn't need a fade fallback the way a modal would β€” it's just there in the layout when the user lands.

Distinct From ToastNotification

Both communicate state, but they answer different questions:

  • AlertBanner lives in the page. It stays until something dismisses it. It's the right tool for page-level or region-level notices that the user should be able to come back to: form errors, trial expiry, no-results-found inside a filtered list.
  • ToastNotification floats over the page. It's ephemeral β€” appearing for a few seconds and disappearing. It's the right tool for transient confirmations and background events: "Saved!", "Copied to clipboard", "Connection lost".

A useful test: if the user might want to re-read the message thirty seconds later, you want a banner. If the message is a confirmation that an action they just took succeeded, you want a toast.

State Flow Diagram

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   not rendered   β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ parent renders <AlertBanner ... />
                           β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  MOUNT + ANIMATE β”‚  200ms opacity + translateY
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚     VISIBLE      β”‚
                  β”‚   (in flow)      β”‚  role / aria-live announces once
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ user clicks Γ—           β”‚ parent unmounts banner
              β”‚ (only if dismissable)   β”‚
              β–Ό                         β–Ό
         onDismiss?.()             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                    β”‚  GONE    β”‚
              β–Ό                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       parent typically            (no exit animation β€”
       hides via state             instant removal)

Props Reference

Prop Type Default Description
variant 'info' | 'success' | 'warning' | 'error' 'info' Drives both colour palette and ARIA role.
title string '' Optional bold heading line.
message string '' Optional body text under the title.
dismissable boolean false Render the Γ— close button on the right.
onDismiss () => void undefined Fires when the close button is clicked. Parent is responsible for unmounting.
children Snippet undefined Optional snippet for action elements (links, buttons) under the message.
class string '' Extra classes appended to the banner.

Edge Cases

Situation Behaviour
Both title and message are empty but children is given The banner still renders β€” the action area becomes the only content. Useful for compact action banners ("Need help? [Contact support]").
dismissable is true but onDismiss is not supplied The Γ— renders and is clickable, but nothing happens visually β€” the banner has no internal "hidden" state, so the parent must drive removal via its own state.
Variant changes dynamically (e.g. info β†’ error) ARIA role and colour palette update reactively. Screen readers may not re-announce β€” assume the role at first render is what the user hears.
User has prefers-reduced-motion: reduce The 200ms slide-in is skipped; the banner appears statically. The visual / aural cue still fires.
Multiple banners stacked vertically Each fires its own announcement; aggressive variants (error, warning) may interrupt the previous polite ones. Prefer one banner per region.
Banner contains a focusable element via children Tab order works as normal β€” the banner doesn't trap focus. The Γ— button is the last tab stop inside the banner.
Long messages wrap onto multiple lines The icon stays top-aligned with the first line via align-items: flex-start; the body and dismiss button sit alongside.

Dependencies

  • Svelte 5.x β€” $props, $derived, snippets.
  • Zero external runtime dependencies. The four icons are inline SVG, the colour palette is plain CSS, and the slide-in animation is pure CSS keyframes.

File Structure

src/lib/components/AlertBanner.svelte         # component implementation
src/lib/components/AlertBanner.md             # this file (rendered inside ComponentPageShell)
src/lib/components/AlertBanner.test.ts        # vitest unit tests
src/routes/alertbanner/+page.svelte           # demo page

API

04
PropTypeDefaultDescription
variant"info" | "success" | "warning" | "error""info"Tone β€” drives the colour palette, icon, and ARIA role.
titlestringβ€”Optional bold heading rendered above the message.
messagestringβ€”Body copy of the banner. Optional if you supply a children snippet.
dismissablebooleanfalseRender a trailing Γ— button that calls onDismiss.
onDismiss() => voidβ€”Callback fired when the dismiss button is pressed.
childrenSnippetβ€”Optional inline content (links, buttons) rendered inside the banner.
classstring""Extra class names forwarded to the root element.