Exclusivebeta

Alert Phase — <mv-alert-phase>

An incident status whose severity decays over time instead of flipping between red and green. Signals (signal(level, { reason }), a click on any [data-alert-phase-signal] trigger, or DOM events mapped with listen) raise it to Alert, Elevated or Caution; each phase runs its own visible countdown (a depleting ring and, in the panel, a decay path to Clear) and, when it times out without a new signal, steps down one level: Alert → Elevated → Caution → Clear. A signal of the same level re-arms the phase, a higher one escalates at once through the cancelable mv-escalate (maintenance windows, deduplication), a lower one is only noted, and mv-decay lets the app keep a phase a little longer. The badge variant is an inline chip for tables and headers; the panel variant is an on-call card with the countdown, time to Clear, last reason, signal count, a patterned timeline of the last minutes with a tick per signal, a transition log and Acknowledge / Hold / Resolve actions. Deadlines are chained from Date.now(), so a throttled background tab catches up exactly, state is JSON-serializable (state / restore() / persist) for reloads and server hydration, and every phase has its own icon, label and pattern (solid, stripes, dots, flat) on top of its color.

CategoryFeedback
TypeWeb Component (<mv-alert-phase>)
Statusbeta
Also installsbutton
Keywordsexclusive, culture, status, incident, severity, alerting, decay, cooldown, monitoring, on-call, observability, countdown, timer, dashboard, security, fraud, ci, timeline, health-check, escalation

When to use

Avoid when

Install

node scripts/add.mjs alert-phase --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["alert-phase"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/alert-phase/alert-phase.js, components/alert-phase/alert-phase.css, components/button/button.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="ap-demo" style="width:min(100%,68rem);margin-inline:auto">
  <style>
    #ap-demo { display:grid; gap:1rem }
    #ap-demo .ap-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap }
    #ap-demo .ap-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #ap-demo .ap-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ap-demo .ap-layout { display:grid; grid-template-columns:minmax(0,1fr) 21rem; gap:1rem; align-items:start }
    #ap-demo .ap-main { display:grid; gap:.75rem; min-width:0 }
    #ap-demo .ap-controls { display:grid; gap:.75rem; padding:.875rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
    #ap-demo .ap-row { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #ap-demo .ap-k { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase; margin-inline-end:.25rem }
    #ap-demo .ap-row .mv-choice { font-size:.8125rem }
    #ap-demo .ap-log { margin:0; min-height:1.1rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ap-demo .ap-services { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #ap-demo .ap-services header { display:flex; align-items:center; justify-content:space-between; gap:.5rem; padding:.875rem 1rem; border-bottom:1px solid var(--mv-border) }
    #ap-demo .ap-services h4 { margin:0; font-size:.875rem }
    #ap-demo .ap-list { list-style:none; margin:0; padding:.25rem 0 }
    #ap-demo .ap-list li { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.625rem 1rem }
    #ap-demo .ap-list li + li { border-top:1px solid var(--mv-border) }
    #ap-demo .ap-svc { display:grid; gap:.125rem; min-width:0 }
    #ap-demo .ap-svc b { font-size:.8125rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ap-demo .ap-svc span { color:var(--mv-fg-muted); font-size:.75rem }
    #ap-demo .ap-foot { display:grid; gap:.625rem; padding:.875rem 1rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #ap-demo .ap-foot p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.5 }
    @media (max-width:56rem) { #ap-demo .ap-layout { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="ap-top">
    <div>
      <h3>Incident console</h3>
      <p>Acme Commerce · production · accelerated timers for the demo (Alert 20 s → Elevated 25 s → Caution 30 s)</p>
    </div>
    <span class="mv-badge" data-variant="outline" data-shape="pill">On call: Priya Natarajan</span>
  </div>

  <div class="ap-layout">
    <div class="ap-main">
      <mv-alert-phase id="ap-checkout" variant="panel" label="Checkout API · us-east-1"
        alert-duration="20s" elevated-duration="25s" caution-duration="30s" history-window="3m">
        <a slot="actions" class="mv-button" data-variant="link" data-size="sm" href="#runbook">Open runbook</a>
      </mv-alert-phase>

      <div class="ap-controls">
        <div class="ap-row">
          <span class="ap-k">Send a signal</span>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="alert" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="5xx rate 6.1% on /v2/charges (threshold 2%)">Error spike · Alert</button>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="elevated" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="Retry rate 18% from mobile clients">Retry storm · Elevated</button>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="caution" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="p95 latency 910 ms (target 600 ms)">Slow responses · Caution</button>
        </div>
        <div class="ap-row">
          <label class="mv-choice">
            <input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="ap-maint">
            <span class="mv-choice-text"><span class="mv-choice-title">Maintenance window: veto escalations</span></span>
          </label>
        </div>
        <p class="ap-log" id="ap-log" aria-hidden="true">Waiting for the next transition…</p>
      </div>
    </div>

    <aside class="ap-services" aria-label="Service status">
      <header>
        <h4>Services</h4>
        <label class="mv-choice" data-control="end" style="font-size:.75rem">
          <input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="ap-sim" checked>
          <span class="mv-choice-text"><span class="mv-choice-title">Live signals</span></span>
        </label>
      </header>
      <ul class="ap-list">
        <li><span class="ap-svc"><b>Payments gateway</b><span>eu-west-1</span></span>
          <mv-alert-phase id="ap-s1" label="Payments gateway" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Identity (SSO)</b><span>global</span></span>
          <mv-alert-phase id="ap-s2" label="Identity" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Search</b><span>us-west-2</span></span>
          <mv-alert-phase id="ap-s3" label="Search" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Media CDN</b><span>global edge</span></span>
          <mv-alert-phase id="ap-s4" label="Media CDN" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Notifications</b><span>ap-southeast-1</span></span>
          <mv-alert-phase id="ap-s5" label="Notifications" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Ledger sync</b><span>eu-central-1</span></span>
          <mv-alert-phase id="ap-s6" label="Ledger sync" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
      </ul>
      <div class="ap-foot">
        <p>Each status decays one level when its timer runs out without a new signal. A new signal of the same level re-arms it; a higher one escalates at once.</p>
      </div>
    </aside>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-alert-phase");
    const root = document.getElementById("ap-demo");
    const $ = (id) => document.getElementById(id);
    const s = 1000;
    const t = Date.now();

    // Hydrate from a server snapshot: the incident started two minutes ago.
    $("ap-checkout").restore({
      phase: "alert",
      remaining: 17 * s,
      since: t - 9 * s,
      episode: { start: t - 118 * s, signals: 9 },
      last: { level: "alert", reason: "Card authorization timeouts: 312 in the last minute", at: t - 3 * s },
      history: [
        { type: "change", from: "clear", to: "caution", at: t - 118 * s, cause: "signal", reason: "p95 latency 842 ms (target 600 ms)" },
        { type: "change", from: "caution", to: "alert", at: t - 97 * s, cause: "signal", reason: "5xx rate 4.2% on /v2/charges" },
        { type: "change", from: "alert", to: "elevated", at: t - 71 * s, cause: "timeout", idle: 20 * s },
        { type: "change", from: "elevated", to: "caution", at: t - 46 * s, cause: "timeout", idle: 25 * s },
        { type: "change", from: "caution", to: "alert", at: t - 9 * s, cause: "signal", reason: "Card authorization timeouts: 188 in the last minute" },
      ],
      signals: [118, 111, 97, 94, 91, 50, 9, 6, 3].map((ago, i) => ({ at: t - ago * s, level: i < 2 ? 1 : i === 5 ? 2 : 3 })),
    });

    const seed = {
      "ap-s1": { phase: "elevated", remaining: 12 * s, last: { level: 3, reason: "Issuer declines up 3x", at: t - 30 * s } },
      "ap-s2": { phase: "alert", held: true, remaining: 16 * s, acknowledged: { by: "Priya N." }, last: { level: 3, reason: "SAML assertion failures", at: t - 40 * s } },
      "ap-s3": { phase: "caution", remaining: 19 * s },
      "ap-s4": { phase: "caution", remaining: 6 * s },
      "ap-s5": { phase: "clear" },
      "ap-s6": { phase: "clear" },
    };
    for (const [id, state] of Object.entries(seed)) $(id).restore(state);

    // Maintenance window: the app vetoes escalations (the cancelable hook).
    const maint = $("ap-maint");
    $("ap-checkout").addEventListener("mv-escalate", (e) => { if (maint.checked) e.preventDefault(); });

    const log = $("ap-log");
    root.addEventListener("mv-change", (e) => {
      const { from, to, cause } = e.detail;
      const name = e.target.label ?? e.target.id;
      log.textContent = `mv-change · ${name} · ${from} → ${to} (${cause})`;
    });
    root.addEventListener("mv-signal", (e) => {
      if (e.detail.outcome === "vetoed") log.textContent = `mv-escalate vetoed · ${e.target.label} · ${e.detail.level}`;
    });
    root.addEventListener("click", (e) => { if (e.target.closest("a[href='#runbook']")) e.preventDefault(); });

    // Live signals on the service list (your WebSocket or polling handler would do this).
    const feed = [
      ["ap-s1", "elevated", "Issuer declines up 3x"], ["ap-s1", "alert", "Acquirer timeouts above 5%"],
      ["ap-s3", "caution", "Index lag 42 s"], ["ap-s3", "elevated", "Query errors 2.4%"],
      ["ap-s4", "caution", "Origin fetch p95 1.2 s"], ["ap-s5", "caution", "Push queue depth 18k"],
      ["ap-s6", "elevated", "Reconciliation drift $1,240"], ["ap-s5", "alert", "APNs rejects 9%"],
      ["ap-s4", "elevated", "Cache hit ratio 71%"],
    ];
    let i = 0;
    const sim = $("ap-sim");
    const timer = setInterval(() => {
      if (!root.isConnected) return clearInterval(timer);
      if (!sim.checked || document.hidden) return;
      const [id, level, reason] = feed[i++ % feed.length];
      $(id).signal(level, { reason, source: "metrics" });
    }, 5000);
  </script>
</div>

Cultural reference

Metal Gear Solid — Hideo Kojima (Konami) (1998, game). Once guards spot the player, the base goes into Alert, then decays through Evasion and Caution back to normal, each phase on its own visible countdown that only a new sighting can reset or send straight back to Alert. In the UI, incident signals raise a status that steps down one level at a time on its own timers, so severity fades gracefully instead of flipping from red to green, and any new signal re-arms or escalates it.

API

Attributes

NameTypeDefaultDescription
variantbadge | panelbadgebadge: inline chip (icon with countdown ring, phase, time left). panel: card with big countdown, decay path, last signal, history timeline, log and actions.
labelstringStatusWhat the status is about ("Checkout API"). Panel title, prefix of the badge's accessible name and of every announcement.
phasealert | elevated | caution | clearInitial phase at first connection (full duration). Changing it later forces that phase (authoritative: not cancelable). The live phase is reflected in data-phase, not here.
alert-durationtime ("2m", "90s", "1h30m", ms number)2mHow long Alert lasts without a new alert-level signal before stepping down to Elevated.
elevated-durationtime5mHow long Elevated lasts before stepping down to Caution.
caution-durationtime10mHow long Caution lasts before Clear. Duration changes apply from the next phase.
heldbooleanFreezes the countdown (the Hold action sets it): no step-down until removed, signals still re-arm and escalate. Removed automatically when the status reaches Clear.
history-windowtime15mSpan of the panel's timeline strip (a block per non-clear stretch, a tick per signal).
log-sizenumber4Transitions listed under the panel's timeline, newest first (0 hides the log).
announceall | escalations | noneallWhat is spoken: escalations assertively, step-downs, Clear, acknowledgments and holds politely. Use escalations on long lists of badges so only rising severity interrupts.
listenstringSpace-separated event:level pairs listened on document, e.g. "checkout-5xx:alert checkout-slow:caution". The event detail may carry { level, reason (or message), source }; without a level in the pair, detail.level or alert is used.
persiststringlocalStorage key: the state is saved on every change and restored (with elapsed time caught up) after a reload. Storage failures are ignored.
actions"false""false" hides the built-in Acknowledge / Hold / Resolve buttons of the panel (slot="actions" content stays).
localeBCP 47 tagen-USLocale of the times in the panel (Intl.DateTimeFormat).
data-alert-phase-signalalert | elevated | cautionOn any clickable element: a click sends that signal to the element whose id is in data-alert-phase-target (space-separated ids) or, without it, to the enclosing mv-alert-phase. data-alert-phase-reason gives the reason.
data-alert-phase-actionacknowledge | hold | resolveOn any clickable element: runs that action on the target (same targeting rules; hold toggles). data-alert-phase-by names who acknowledged.
data-phase / data-level / data-held / data-acknowledgedset by the componentLive state on the host for styling (data-level 0..3).

Properties

NameTypeDescription
phase"alert" | "elevated" | "caution" | "clear"Current phase. Setting it forces that phase (not cancelable, announced).
level0..3Current level: 0 clear, 1 caution, 2 elevated, 3 alert (read-only).
remainingnumber (ms)Time left in the running phase, recomputed on every read; frozen while held, 0 when clear (read-only).
remainingToClearnumber (ms)Time before Clear if no new signal arrives: remaining plus the lower phases' durations (read-only).
deadlinenumber (epoch ms) | nullWhen the running phase steps down; null when clear or held (read-only).
heldbooleanMirrors the held attribute.
acknowledged{ by, at } | nullAcknowledgment of the current episode; reset by any escalation and by Clear (read-only).
lastSignal{ level, phase, reason, source, at, outcome } | nullMost recent signal, whatever its outcome (escalated, rearmed, noted, vetoed), read-only. The panel marks a vetoed one.
historyArray<{ type, at, from?, to?, cause?, reason?, idle?, by?, phase? }>Up to 200 entries, oldest first. type: change (cause: signal | timeout | resolve | api | initial; idle = ms without signal for timeouts), ack, hold, resume, veto (read-only copy).
stateobjectJSON-safe snapshot { phase, level, deadline, remaining, held, acknowledged, since, quietSince, episode, last, history, signals }: send it to your server or storage and give it back to restore().
stringsPartial<Record<string, string>>Overrides for every visible text and announcement: phase names (alert, elevated, caution, clear), next, nextClear, heldText, quiet, lastSignal, signalsValue ({count}), acknowledge, hold, resume, resolve, timer ({time}), logTimeout, announceEscalate ({label}, {phase}), announceStep, announceClear… English defaults.

Methods

NameDescription
signal(level = "alert", { reason?, source? })Reports a signal. Same level as the current phase → re-armed to its full duration; higher → escalation (mv-escalate, cancelable); lower → only noted (counted, shown as last signal). Returns "escalated" | "rearmed" | "noted" | "vetoed" | "ignored".
acknowledge({ by? })Marks the current episode as seen: the Alert beacon stops, the panel shows who acknowledged. Returns false when clear or already acknowledged.
hold() / release()Freezes / resumes the countdown (sets / removes held). Emits mv-hold.
resolve({ reason? })Back to Clear now. Returns false when already clear.
restore(state)Rebuilds the status from a state snapshot (or a partial one: { phase, remaining } is enough), then catches up the time elapsed since, step by step. Emits the resulting mv-change events, without announcing them.
clearHistory()Forgets the history and the signal ticks; the current phase is kept.

Events

NameDescription
mv-escalateCancelable, before a signal raises the phase. detail: { from, to, reason, source }. preventDefault() vetoes it (maintenance window, known flapping check): the signal is logged as vetoed and the phase is unchanged.
mv-decayCancelable, before a timeout steps the phase down. detail: { from, to, at }. preventDefault() keeps the phase and re-arms it for its full duration (e.g. while a deploy is still rolling out).
mv-changeAfter every transition. detail: { from, to, level, cause: "signal" | "timeout" | "resolve" | "api", reason, source, at, deadline }. at is the exact time of the step, also when a background tab catches up.
mv-signalAfter every signal. detail: { level, reason, source, outcome, phase } (outcome as returned by signal(), phase = resulting phase).
mv-acknowledgeThe episode was acknowledged. detail: { phase, by, at }.
mv-holdThe countdown was frozen or resumed. detail: { held, remaining, phase }.

Content structure

NameDescription
actionsChildren with slot="actions" ("Open runbook", "Page on-call"…) are placed at the end of the panel's actions row; hidden in the badge variant.

CSS classes

NameDescription
mv-alert-phase-badgeBadge root (data-phase, data-flash on escalation): .mv-alert-phase-icon (countdown ring), -name, -time (-time-held pause glyph).
mv-alert-phase-panelPanel root (role=group, data-phase): -head, -title, -flag (ack / held), -now (icon, -name, -next, -clock), -path (-track, -seg, -seg-fill, -end, -steps, -step), -facts, -history (-strip, -block, -tick, -axis, -log), -actions.
mv-alert-phase-swatchAny element painted with a phase's tone and pattern (set data-phase on it): segment fills, timeline blocks, log dots. Reusable for a legend.

CSS variables

NameDefaultDescription
--mv-alert-phase-alertvar(--mv-danger)Alert tone (solid pattern).
--mv-alert-phase-elevatedmix of --mv-danger and --mv-warningElevated tone (diagonal stripes).
--mv-alert-phase-cautionvar(--mv-warning)Caution tone (dots).
--mv-alert-phase-clearvar(--mv-success)Clear tone (flat tint).

Accessibility

Severity is never carried by color alone: each phase has its own icon (octagon, magnifier, eye, check), its name in text and its own pattern (solid, stripes, dots, flat), which also keeps the phases apart in forced colors where every tone becomes CanvasText. Changes are announced through two visually hidden live regions: an escalation assertively with its reason (“Checkout API: Alert. 5xx rate 6.1%…”), step-downs, Clear, acknowledgments and holds politely; re-armed phases and the ticking countdown are never announced, and announce="escalations" or "none" keeps long lists of badges quiet. The remaining time is exposed in a role="timer" element right after the phase name (“Checkout API: Alert, steps down in 17 seconds”), whose implicit aria-live is off, so it is read on demand without chatter. The panel is a labelled group; the timeline strip and decay path are aria-hidden and duplicated by text (next step, time to Clear, last signal, signal count, a real <ol> log with <time datetime>). Acknowledge, Hold (aria-pressed toggle) and Resolve are native buttons; when they don't apply they become aria-disabled, so focus is never lost. Declarative triggers work with any native button, keyboard included. Reduced motion (OS or data-motion="reduce"): the ring and bars jump once per second instead of sliding, the Alert beacon becomes a static ring and the escalation flash is skipped. Performance: one timeout per second at most, aligned on the displayed second; badges sleep entirely while clear or held.