Skip to content

cv-toast-region

Container that manages a queue of toast notifications with positioning, stacking layout, and auto-dismiss lifecycle.

Headless: createToast

Usage

View source
html
<div class="toast-demo-shell" data-demo="toast" data-live-demo-height="760">
  <section class="toast-demo-hero" aria-labelledby="toast-demo-title">
    <div class="toast-demo-copy">
      <span class="toast-demo-kicker">Queued live feedback</span>
      <h3 id="toast-demo-title">Use toast for non-blocking workflow feedback that can stack, pause, and expire.</h3>
      <p>
        The region owns queue visibility, ARIA contracts, and auto-dismiss timing. Callers push domain events into
        the controller and keep focus on the current task.
      </p>
    </div>

    <dl class="toast-demo-metrics" aria-label="Toast contract summary">
      <div>
        <dt>Region role</dt>
        <dd>region / polite</dd>
      </div>
      <div>
        <dt>Visible limit</dt>
        <dd>max-visible</dd>
      </div>
      <div>
        <dt>Item roles</dt>
        <dd>status / alert</dd>
      </div>
    </dl>
  </section>

  <section class="toast-demo-workbench" aria-label="Toast queue workbench">
    <div class="toast-demo-panel">
      <div class="toast-demo-section-header">
        <span class="toast-demo-kicker">Vault workflow events</span>
        <h4>Push realistic status changes into one region and inspect queue state.</h4>
      </div>

      <div class="toast-demo-actions" aria-label="Toast scenarios">
        <cv-button data-toast-action="saved" variant="primary">Save profile</cv-button>
        <cv-button data-toast-action="route">Route proof</cv-button>
        <cv-button data-toast-action="warning">Warn handoff</cv-button>
        <cv-button data-toast-action="error" variant="danger">Block export</cv-button>
        <cv-button data-toast-action="loading">Relay sync</cv-button>
      </div>

      <div class="toast-demo-controls" aria-label="Toast region controls">
        <div class="toast-demo-control-group">
          <span class="toast-demo-label">Position</span>
          <div class="toast-demo-segment" role="group" aria-label="Toast position">
            <button type="button" data-toast-position="top-end" aria-pressed="true">Top end</button>
            <button type="button" data-toast-position="bottom-end" aria-pressed="false">Bottom end</button>
            <button type="button" data-toast-position="bottom-center" aria-pressed="false">Bottom center</button>
          </div>
        </div>

        <div class="toast-demo-control-group">
          <span class="toast-demo-label">Max visible</span>
          <div class="toast-demo-segment" role="group" aria-label="Maximum visible toasts">
            <button type="button" data-toast-limit="2" aria-pressed="false">2</button>
            <button type="button" data-toast-limit="3" aria-pressed="true">3</button>
            <button type="button" data-toast-limit="4" aria-pressed="false">4</button>
          </div>
        </div>
      </div>

      <div class="toast-demo-footer-actions" aria-label="Queue actions">
        <cv-button data-toast-action="burst">Queue burst</cv-button>
        <cv-button data-toast-action="clear">Clear queue</cv-button>
      </div>
    </div>

    <div class="toast-demo-stage" aria-label="Live toast region preview">
      <div class="toast-demo-route-card">
        <span class="toast-demo-kicker">Visible route</span>
        <strong>travel-profile.visible</strong>
        <p>Task content stays interactive while feedback is announced from the toast region.</p>
      </div>

      <cv-toast-region data-toast-region position="top-end" max-visible="3"></cv-toast-region>
    </div>

    <aside class="toast-demo-side" aria-label="Toast queue state">
      <dl class="toast-demo-state" aria-label="Current toast state">
        <div>
          <dt>Visible</dt>
          <dd data-toast-visible>0</dd>
        </div>
        <div>
          <dt>Queued</dt>
          <dd data-toast-queued>0</dd>
        </div>
        <div>
          <dt>Position</dt>
          <dd data-toast-current-position>top-end</dd>
        </div>
        <div>
          <dt>Last event</dt>
          <dd data-toast-event>idle</dd>
        </div>
      </dl>

      <p class="toast-demo-log" role="status" aria-live="polite" data-toast-log>
        Waiting for a workflow event. Push a scenario or queue a burst.
      </p>
    </aside>
</div>

<script type="module">
  const scenarios = {
    saved: {
      level: 'success',
      icon: 'check-circle-fill',
      title: 'Visible profile saved',
      message: 'Policy and route labels were written to the visible namespace.',
      durationMs: 0,
      progress: false,
    },
    route: {
      level: 'info',
      icon: 'info',
      title: 'Route proof ready',
      message: 'Relay path proof is available without opening a blocking dialog.',
      durationMs: 0,
      progress: false,
      actions: ['Inspect', 'Pin'],
    },
    warning: {
      level: 'warning',
      icon: 'exclamation-triangle',
      title: 'Handoff window closing',
      message: 'Confirm operator handoff before leaving the recovery route.',
      durationMs: 0,
      progress: false,
    },
    error: {
      level: 'error',
      icon: 'x-circle-fill',
      title: 'Export blocked',
      message: 'Coercion profile is active. Hidden namespaces stay unavailable.',
      durationMs: 0,
      progress: false,
    },
    loading: {
      level: 'loading',
      title: 'Relay sync in progress',
      message: 'Timers pause while the pointer is over the stack.',
      durationMs: 8200,
      progress: true,
      closable: false,
    },
  }

  document.querySelectorAll('.toast-demo-shell[data-demo="toast"]:not([data-ready])').forEach((shell) => {
    shell.dataset.ready = 'true'

    const region = shell.querySelector('[data-toast-region]')
    const visible = shell.querySelector('[data-toast-visible]')
    const queued = shell.querySelector('[data-toast-queued]')
    const currentPosition = shell.querySelector('[data-toast-current-position]')
    const lastEvent = shell.querySelector('[data-toast-event]')
    const log = shell.querySelector('[data-toast-log]')
    const positionButtons = [...shell.querySelectorAll('[data-toast-position]')]
    const limitButtons = [...shell.querySelectorAll('[data-toast-limit]')]

    const setText = (target, value) => {
      if (target) target.textContent = value
    }

    const items = () => region?.controller.model.state.items() ?? []
    const visibleItems = () => region?.controller.model.state.visibleItems() ?? []

    const syncState = (eventName) => {
      const itemCount = items().length
      const visibleCount = visibleItems().length

      shell.dataset.toastQueue = itemCount > 0 ? 'active' : 'empty'
      setText(visible, String(visibleCount))
      setText(queued, String(Math.max(itemCount - visibleCount, 0)))
      setText(currentPosition, region?.position ?? 'top-end')
      setText(lastEvent, eventName)
    }

    const updatePressed = (buttons, attr, value) => {
      buttons.forEach((button) => {
        button.setAttribute('aria-pressed', button.getAttribute(attr) === value ? 'true' : 'false')
      })
    }

    const pushScenario = (name) => {
      const scenario = scenarios[name]
      if (!region || !scenario) return

      const toast = {
        ...scenario,
        actions: scenario.actions?.map((label) => ({
          label,
          onClick: () => {
            setText(log, `${label}: action handled inside the toast item.`)
            syncState(`action:${label.toLowerCase()}`)
          },
        })),
      }

      region.controller.push(toast)
      setText(log, `${scenario.title}: ${scenario.message}`)
      syncState(`push:${name}`)
    }

    const queueBurst = () => {
      ;['saved', 'route', 'warning', 'error'].forEach(pushScenario)
      setText(log, 'Burst queued four events. The newest items stay visible first.')
      syncState('burst')
    }

    const setPosition = (position) => {
      if (!region) return
      region.position = position
      region.setAttribute('position', position)
      updatePressed(positionButtons, 'data-toast-position', position)
      syncState('position')
    }

    const setMaxVisible = async (value) => {
      if (!region) return
      region.maxVisible = value
      region.setAttribute('max-visible', String(value))
      await region.updateComplete
      updatePressed(limitButtons, 'data-toast-limit', String(value))
      syncState('max-visible')
    }

    shell.querySelector('[data-toast-action="saved"]')?.addEventListener('click', () => {
      pushScenario('saved')
    })

    shell.querySelector('[data-toast-action="route"]')?.addEventListener('click', () => {
      pushScenario('route')
    })

    shell.querySelector('[data-toast-action="warning"]')?.addEventListener('click', () => {
      pushScenario('warning')
    })

    shell.querySelector('[data-toast-action="error"]')?.addEventListener('click', () => {
      pushScenario('error')
    })

    shell.querySelector('[data-toast-action="loading"]')?.addEventListener('click', () => {
      pushScenario('loading')
    })

    shell.querySelector('[data-toast-action="burst"]')?.addEventListener('click', queueBurst)

    shell.querySelector('[data-toast-action="clear"]')?.addEventListener('click', () => {
      region?.controller.clear()
      setText(log, 'Queue cleared. The region is idle.')
      syncState('clear')
    })

    positionButtons.forEach((button) => {
      button.addEventListener('click', () => {
        setPosition(button.getAttribute('data-toast-position') ?? 'top-end')
      })
    })

    limitButtons.forEach((button) => {
      button.addEventListener('click', () => {
        void setMaxVisible(Number(button.getAttribute('data-toast-limit') ?? '3'))
      })
    })

    region?.addEventListener('cv-close', (event) => {
      setText(log, `cv-close: ${event.detail.id} left the queue.`)
      syncState('cv-close')
    })

    window.setTimeout(() => {
      pushScenario('saved')
      pushScenario('warning')
    }, 220)
  })
</script>

Anatomy

<cv-toast-region> (host)
└── <section part="base" role="region" aria-live="polite" aria-atomic="false">
    ├── <cv-toast part="item" role="status|alert" data-level="info">
    ├── <cv-toast part="item" role="alert" data-level="error">
    └── …

Attributes

AttributeTypeDefaultDescription
positionString"top-end"Positioning of the region: top-start | top-center | top-end | bottom-start | bottom-center | bottom-end
max-visibleNumber3Maximum number of toasts displayed simultaneously

Slots

SlotDescription
(none)Content is rendered programmatically from the toast queue; no user-facing slots

CSS Parts

PartElementDescription
base<section>Root region element with role="region" and aria-live

CSS Custom Properties

PropertyDefaultDescription
--cv-toast-region-gapvar(--cv-space-2, 8px)Spacing between stacked toasts
--cv-toast-region-insetvar(--cv-space-4, 16px)Distance from viewport edges
--cv-toast-region-positionfixedCSS positioning mode for the region
--cv-toast-region-widthautoInline size of the toast region
--cv-toast-region-z-index9999Stacking order above page content
--cv-toast-region-max-width420pxMaximum width of the toast region

Visual States

Host selectorDescription
:host([position="top-start"])Region fixed to top-left
:host([position="top-center"])Region fixed to top-center
:host([position="top-end"])Region fixed to top-right (default)
:host([position="bottom-start"])Region fixed to bottom-left
:host([position="bottom-center"])Region fixed to bottom-center
:host([position="bottom-end"])Region fixed to bottom-right

Events

EventDetailDescription
cv-close{ id: string }Fires when a toast is removed from the queue (auto-dismiss or explicit dismiss)

Reactive State Mapping

cv-toast-region is a visual adapter over headless createToast.

UIKit PropertyDirectionHeadless Binding
max-visibleattr → optionPassed as maxVisible in createToast(options)
(controller)propertycreateToastController() wraps createToast() and exposes push, dismiss, clear, pause, resume
Headless StateDirectionDOM Reflection
state.visibleItems()state → renderDrives the rendered list of cv-toast items
state.isPaused()state → internalTracked internally; toggled by mouseenter/mouseleave on [part="base"]
  • contracts.getRegionProps() is spread onto the inner [part="base"] element to apply id, role="region", aria-live, and aria-atomic.
  • mouseenter on [part="base"] calls actions.pause(); mouseleave calls actions.resume().
  • UIKit tracks the previous set of toast IDs across renders and dispatches a cv-close event for each ID that disappears from the queue.
  • UIKit does not own queue logic, timer management, or visibility slicing; headless state is the source of truth.

Child Elements

cv-toast

Individual toast notification item rendered within cv-toast-region. Displays a message with severity-based styling, an optional icon, and an optional dismiss button.

Anatomy

<cv-toast> (host)
└── <div part="base" role="status|alert" data-level="info">
    ├── <span part="icon">
    │   └── <slot name="icon">         ← severity icon
    ├── <div part="content">
    │   └── <span part="label">
    │       └── <slot>                  ← message text
    └── <button part="dismiss">        ← only when closable

Attributes

AttributeTypeDefaultDescription
levelString"info"Severity level: info | success | warning | error
closableBooleantrueWhether the dismiss button is shown
toast-idString""Identifier for the toast, included in the cv-close event detail

Slots

SlotDescription
(default)Message text content
iconSeverity icon; overrides the default icon for the level

CSS Parts

PartElementDescription
base<div>Root wrapper for the toast item with role and data-level
icon<span>Wrapper around the icon slot
content<div>Wrapper around the message area
label<span>Wrapper around the default slot (message text)
dismiss<button>Dismiss/close button

CSS Custom Properties

PropertyDefaultDescription
--cv-toast-padding-inlinevar(--cv-space-3, 12px)Horizontal padding
--cv-toast-padding-blockvar(--cv-space-2, 8px)Vertical padding
--cv-toast-border-radiusvar(--cv-radius-md, 10px)Border radius
--cv-toast-gapvar(--cv-space-2, 8px)Spacing between icon, content, and dismiss button
--cv-toast-backgroundvar(--cv-color-surface-elevated, #1d2432)Background color
--cv-toast-border-colorvar(--cv-color-border, #2a3245)Default border color
--cv-toast-colorvar(--cv-color-text, #e8ecf6)Text color
--cv-toast-shadowvar(--cv-shadow-1, 0 2px 8px rgba(0, 0, 0, 0.24))Box shadow

Additionally, component styles depend on theme tokens through fallback values:

Theme PropertyDefaultDescription
--cv-color-border#2a3245Base border color
--cv-color-surface-elevated#1d2432Elevated surface background
--cv-color-text#e8ecf6Default text color
--cv-color-text-muted#9aa6bfMuted text color (dismiss button)
--cv-color-primary#65d7ffPrimary accent (focus ring)
--cv-color-success#6ef7c8Success tint for border
--cv-color-warning#ffd36eWarning tint for border
--cv-color-danger#ff7d86Error/danger tint for border
--cv-radius-md10pxMedium border radius
--cv-radius-sm6pxSmall border radius (dismiss button)
--cv-space-28pxSpacing scale
--cv-space-312pxSpacing scale

Visual States

Host selector / Part selectorDescription
[data-level="info"]Default styling with standard border
[data-level="success"]Border tinted with --cv-color-success via color-mix()
[data-level="warning"]Border tinted with --cv-color-warning via color-mix()
[data-level="error"]Border tinted with --cv-color-danger via color-mix()

Events

EventDetailDescription
cv-close{ id: string }Fires when the dismiss button is clicked; bubbles and is composed

Reactive State Mapping

Each toast item is bound to headless contracts per toast ID.

Headless ContractDirectionDOM Binding
contracts.getToastProps(id)state → attrsSpread onto [part="base"]: id, role (status or alert), data-level
contracts.getDismissButtonProps(id)state → attrsSpread onto [part="dismiss"]: id, role="button", tabindex="0", aria-label, onClick handler
  • role is determined by the headless contract based on level: role="status" for info/success, role="alert" for warning/error.
  • In standalone usage, cv-toast computes role from its level property matching the headless contract logic.
  • When rendered within cv-toast-region, the region spreads headless contract props onto each inline toast item.
  • The dismiss button onClick handler from getDismissButtonProps(id) calls actions.dismiss(id) internally.

ChromVoid UIKit documentation