Exclusivebeta

Accounts — <mv-accounts>

Multi-source timeline that lines up several accounts of the same event and shows, fact by fact, where they agree, where they contradict each other and where a source stays silent: incident post-mortems (gateway logs vs payment provider vs client telemetry vs on-call notes), delivery disputes (customer vs warehouse vs carrier), audit trails, fact-checking, meeting notes from several attendees. Each source is a plain <ol data-source> (or the sources property) whose timestamped <li data-at data-key> entries claim facts; entries sharing a key are aligned on one row and judged: agree, conflict (a different data-value, or a time outside the tolerance, with the majority computed per fact and ties settled by an optional reference source) or only-one-source, and a source missing from a row is shown as silent. Three views: Side by side (one lane per source, rows aligned by fact with links drawn across lanes and a consensus-time gutter), Merged (one chronology where each fact lists who backs it, who contradicts it and a consensus meter) and One witness (the story reordered as a single source tells it, the others dimmed alongside). Clocks disagree, so each source has an offset, editable in a Clocks panel or estimated from the facts it shares with the others (Match, Align all clocks); rows reorder with FLIP and changed verdicts flash. A summary (“3 conflicts · 2 facts only one source mentions”) lists every disagreement as a jump link, selecting a fact opens a written verdict under its row, and every status is spelled out in text and drawn with a distinct shape, never color alone.

CategoryData display
TypeWeb Component (<mv-accounts>)
Statusbeta
Keywordsexclusive, culture, timeline, multi-source, reconciliation, post-mortem, incident, audit-trail, dispute, fact-check, comparison, consensus, conflict, clock-skew, swimlanes, logs, provenance, keyboard

When to use

Avoid when

Install

node scripts/add.mjs accounts --out ./src/marvelous

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

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, components/accounts/accounts.js, components/accounts/accounts.css.

Usage

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

<div id="acc-demo" style="width:min(100%,78rem);margin-inline:auto">
  <style>
    #acc-demo { display:grid; gap:2.25rem }
    #acc-demo .acc-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap; margin:0 0 .875rem }
    #acc-demo .acc-kicker { display:flex; align-items:center; gap:.5rem; margin:0 0 .375rem; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
    #acc-demo .acc-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #acc-demo .acc-sub { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:62ch }
    #acc-demo .acc-side { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
    #acc-demo .acc-side .mv-choice { font-size:.8125rem }
    #acc-demo .acc-log { margin:.625rem 0 0; min-height:1.25rem; color:var(--mv-fg-muted); font-size:.75rem }
    #acc-demo .acc-log b { color:var(--mv-fg); font-weight:600 }
    #acc-demo code { font-family:var(--mv-font-mono); font-size:.92em }
  </style>

  <section aria-labelledby="acc-t1">
    <div class="acc-head">
      <div>
        <p class="acc-kicker"><span class="mv-badge" data-variant="danger" data-shape="pill">SEV-2</span> INC-2291 · Sep 14, 2026 · post-mortem draft</p>
        <h3 id="acc-t1">Checkout payments failing for 12 minutes</h3>
        <p class="acc-sub">Four sources, one outage. Rows line up the same fact across every account; select one to read the verdict, or switch to Merged and One witness.</p>
      </div>
      <div class="acc-side">
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="acc-skew" checked>
          <span class="mv-choice-text"><span class="mv-choice-title">Correct the mobile app clock</span></span>
        </label>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="acc-next">Next conflict</button>
      </div>
    </div>

    <mv-accounts id="acc-incident" tolerance="1m" time-zone="UTC" label="Incident accounts" selected="degraded">
      <ol data-source="API gateway" data-id="gateway" data-description="Edge access logs">
        <li data-at="2026-09-14T14:01:05Z" data-key="deploy" data-label="Deploy v4.18.0">Config reload after deploy <code>v4.18.0</code></li>
        <li data-at="2026-09-14T14:02:29Z" data-key="degraded" data-label="Payments start timing out">First upstream timeout to payments (30 s)</li>
        <li data-at="2026-09-14T14:02:41Z" data-key="first-502" data-value="502" data-label="First 502 at checkout">on <code>POST /checkout</code></li>
        <li data-at="2026-09-14T14:08:10Z" data-key="retries" data-label="Retry storm">Traffic on /checkout up 4×, mostly retries</li>
        <li data-at="2026-09-14T14:12:20Z" data-key="rollback" data-value="v4.17.2" data-label="Rollback">Config reload after rollback</li>
        <li data-at="2026-09-14T14:14:00Z" data-key="recovered" data-label="Checkout recovers">Error rate back under 0.5%</li>
      </ol>
      <ol data-source="Payment provider" data-id="provider" data-description="Webhook log and RCA">
        <li data-at="2026-09-14T14:06:30Z" data-key="degraded">Incident start, per the provider’s RCA</li>
        <li data-at="2026-09-14T14:13:30Z" data-key="charges" data-value="0 captured" data-label="Charges during the outage">No charges captured from 14:02 to 14:14</li>
        <li data-at="2026-09-14T14:14:30Z" data-key="recovered">Status page: resolved</li>
      </ol>
      <ol data-source="Mobile app" data-id="app" data-offset="-2m40s" data-description="Client telemetry">
        <li data-at="2026-09-14T14:05:11Z" data-key="degraded">Card payment spinner, then a timeout</li>
        <li data-at="2026-09-14T14:05:24Z" data-key="first-502" data-value="502">“Something went wrong” shown to 1,204 users</li>
        <li data-at="2026-09-14T14:10:53Z" data-key="retries">Auto-retry fires up to 3× per tap</li>
        <li data-at="2026-09-14T14:16:05Z" data-key="charges" data-value="Charged twice">Support ticket: charged twice for order #88121</li>
        <li data-at="2026-09-14T14:16:40Z" data-key="recovered">Checkout success rate back to 99%</li>
      </ol>
      <ol data-source="On-call notes" data-id="oncall" data-description="Incident channel">
        <li data-at="2026-09-14T14:01:00Z" data-key="deploy">Maya Okafor ships v4.18.0</li>
        <li data-at="2026-09-14T14:07:00Z" data-key="paged" data-label="On-call paged">Paged: checkout error rate at 6%</li>
        <li data-at="2026-09-14T14:12:00Z" data-key="rollback" data-value="v4.17.2">Rolled back to v4.17.2</li>
        <li data-at="2026-09-14T14:18:00Z" data-key="recovered">Declared resolved in #incidents</li>
        <li data-at="2026-09-14T14:25:00Z" data-key="rca" data-label="Suspected cause">New connection-pool limit in v4.18.0</li>
      </ol>
    </mv-accounts>
    <p class="acc-log" id="acc-log" aria-live="off">Tip: Alt + ←/→ jumps to the same fact in the next source, Alt + ↑/↓ to the next disagreement.</p>
  </section>

  <section aria-labelledby="acc-t2">
    <div class="acc-head">
      <div>
        <p class="acc-kicker"><span class="mv-badge" data-variant="warning" data-shape="pill">Dispute</span> Order #88342 · Chicago time</p>
        <h3 id="acc-t2">“My package never arrived”</h3>
        <p class="acc-sub">Customer, warehouse and carrier merged into one chronology: each fact shows who backs it and who contradicts it. Weight readings are compared on value only.</p>
      </div>
    </div>
    <mv-accounts id="acc-dispute" view="merged" tolerance="30m" time-zone="America/Chicago" label="Delivery dispute accounts">
      <ol data-source="Customer" data-description="Support ticket #44710">
        <li data-at="2026-09-08T09:12:00-05:00" data-key="ordered" data-label="Order placed">Placed order #88342 on the website</li>
        <li data-at="2026-09-10T08:05:00-05:00" data-key="out" data-label="Out for delivery">Got the “out for delivery” text</li>
        <li data-at="2026-09-10T18:30:00-05:00" data-key="delivered" data-value="Not received" data-label="Delivery">Nothing at the door when I got home</li>
      </ol>
      <ol data-source="Warehouse" data-description="Scan events">
        <li data-at="2026-09-08T09:13:00-05:00" data-key="ordered">Order received by the warehouse system</li>
        <li data-at="2026-09-08T14:40:00-05:00" data-key="packed" data-label="Packed">Packed and sealed, 2 items</li>
        <li data-at="2026-09-08T14:41:00-05:00" data-key="weight" data-value="1.4 kg" data-tolerance="any" data-label="Parcel weight">Weighed at the packing station</li>
        <li data-at="2026-09-08T17:05:00-05:00" data-key="handoff" data-label="Handed to carrier">Handed over, manifest 7731</li>
      </ol>
      <ol data-source="Carrier" data-description="Tracking history">
        <li data-at="2026-09-08T17:20:00-05:00" data-key="handoff">Picked up at origin</li>
        <li data-at="2026-09-09T03:10:00-05:00" data-key="weight" data-value="0.6 kg" data-tolerance="any">Re-weighed at the regional hub</li>
        <li data-at="2026-09-10T08:02:00-05:00" data-key="out">Out for delivery</li>
        <li data-at="2026-09-10T11:47:00-05:00" data-key="delivered" data-value="Delivered">Left at front door, photo on file</li>
      </ol>
    </mv-accounts>
  </section>

  <script type="module">
    const inc = document.getElementById("acc-incident");
    await customElements.whenDefined("mv-accounts");
    const log = document.getElementById("acc-log");

    // Toggle the known clock drift of the mobile app: uncorrected, its skew fakes conflicts.
    document.getElementById("acc-skew").addEventListener("change", (e) => {
      inc.setOffset("app", e.target.checked ? "-2m40s" : 0);
    });
    inc.addEventListener("mv-offset-change", (e) => {
      if (e.detail.source === "app") document.getElementById("acc-skew").checked = e.detail.offset !== 0;
    });

    document.getElementById("acc-next").addEventListener("click", () => {
      const issues = inc.facts.filter((f) => f.status === "conflict");
      if (!issues.length) return;
      const i = issues.findIndex((f) => f.key === inc.selected);
      inc.select(issues[(i + 1) % issues.length].key, { focus: true });
    });

    inc.addEventListener("mv-select-fact", (e) => {
      const { fact } = e.detail;
      log.replaceChildren("mv-select-fact → ", Object.assign(document.createElement("b"), { textContent: fact.label }), ` · ${fact.status} · ${fact.entries.length} of ${inc.sources.length} sources`);
    });
    inc.addEventListener("mv-view-change", (e) => {
      log.replaceChildren("mv-view-change → ", Object.assign(document.createElement("b"), { textContent: e.detail.view }));
    });
  </script>
</div>

Cultural reference

Rashomon — Akira Kurosawa (1950, film). One event is told four times by four witnesses, each account plausible and each contradicting the others, so the truth can only be triangulated. In the UI, every source's account of an incident becomes a lane, the same facts are lined up across them and judged as agreed, contradicted or unsupported, and the story can be read side by side, merged, or as a single witness tells it.

API

Attributes

NameTypeDefaultDescription
viewlanes | merged | witnesslaneslanes: one column per source, rows aligned by fact. merged: one chronology of facts with their supporters, dissenters and a consensus meter. witness: lanes reordered by the witness source's own chronology, that lane first and wider, the others dimmed.
witnesssource idSource focused by the witness view (default: the first source).
toleranceduration1mTimes closer than this count as the same moment ("90s", "2m", "1h", "500ms" or ms). Per fact, data-tolerance on any entry (or factLabels[key].tolerance) overrides it; "any" means time is not compared for that fact (e.g. a measurement taken twice).
referencesource idSystem of record: wins value and time ties when no majority exists, and is the clock kept fixed by Align all clocks (default anchor: the first source).
selectedfact keySelected fact: highlighted in every view, with a written verdict under its row in the lanes views. Reflects user selection.
filterall | issuesallissues hides facts on which every source agrees (the “Only disagreements” toggle).
localeBCP 47en-USLocale for times, durations, plurals and name lists.
time-zoneIANA zoneZone used to display times (default: the viewer's; UTC when every time is wall-clock only). Shown in the gutter header.
precisionauto | seconds | minutesautoTime display precision; auto shows seconds when any entry has them.
labelstringAccountsAccessible name of the timeline group.
heading-level1–63Level of the lane headings.
controls"true" | "false"true"false" hides the built-in toolbar (views, filter, Clocks) when the app drives them.
data-sourcestringOn a child <ol>/<ul>: the source's display name. Makes it a lane. The list itself is hidden and re-read live when it changes.
data-id / data-offset / data-descriptionstring / duration / stringOn a source list: stable id (default: slug of the name), clock correction added to its times ("-2m40s" for a clock 2 min 40 s fast) and a short description under its heading.
data-atISO date-time | epoch ms | HH:MM[:SS]On an entry: when the source says it happened. Wall-clock times are compared as is. Entries without a time are ordered after the previous timed entry of their source and never conflict on time.
data-keystringOn an entry: the fact it claims. Same key across sources = same fact. Entries without a key are facts only their source mentions.
data-valuestringOn an entry: the comparable claim (status code, amount, outcome). Compared case-, accent- and space-insensitively (or with the compare property); entries without a value never conflict on value.
data-label / data-tolerancestring / duration | "any"On an entry: the fact's title (first one found wins, default: the key humanized) and a per-fact tolerance.

Properties

NameTypeDescription
sourcesArray<{ id?, name, offset?, description?, entries: Array<{ key?, at?, value?, text, label?, tolerance? }> }>Data alternative to the markup (takes precedence over child lists). Reading it returns the current model with live offsets.
factsArray<Fact>Read-only analysis in consensus order: { key, label, status: "agree" | "conflict" | "single", at (consensus ms), conflict: { value, time }, value (majority), entries: [{ source, key, at, reportedAt, value, text, status: "agree" | "dissent" | "split" | "single", delta, node }], silent: [source ids], verdict (sentence) }.
summary{ facts, agreed, conflicts, single, silentGaps }Counts behind the summary line (read-only).
offsetsRecord<sourceId, ms>Current clock corrections (read-only; use setOffset).
factLabelsRecord<key, { label?, tolerance? }>Titles and tolerances per fact, when the markup cannot carry them.
compare(a: string, b: string, key: string) => booleanCustom value equality (numeric tolerance, synonyms…). Default: case-, accent- and whitespace-insensitive.
stringsPartial<Record<string, string>>Overrides for every visible label, verdict sentence and announcement (plural forms as "one|other", placeholders like {names}, {delta}). English defaults.
view / witness / tolerance / reference / selected / filter / locale / timeZone / precision / label / headingLevel / controlsreflectedMirror the attributes.

Methods

NameDescription
select(key, { source?, focus? })Selects a fact (null clears). Emits the cancelable mv-select-fact; focus: true moves focus to its first dissenting entry (or the given source) and scrolls it into view. Returns false if unknown or vetoed.
setOffset(sourceId, value)Corrects a source's clock (ms or duration string). Emits the cancelable mv-offset-change, re-judges every fact and announces the new summary.
suggestOffset(sourceId)Estimated correction for a source: median gap between its times and the other sources' times on the facts they share, rounded to the second. null when it shares nothing.
alignClocks()Applies suggestOffset to every source except the reference (two passes) and returns the offsets. Real disagreements survive because the median ignores a minority of outliers.
refresh()Re-reads the markup (normally automatic through a MutationObserver).

Events

NameDescription
mv-select-factCancelable, before a fact is selected (click, Enter/Space, summary link, select()). detail: { key, fact, source (id of the entry clicked, or null), entry }.
mv-view-changeCancelable, before the view changes. detail: { view, previous, witness }.
mv-offset-changeCancelable, before a clock correction is applied. detail: { source, offset, previous, reason: "input" | "align" | "reset" | "api" }.
mv-reconcileAfter every analysis (data, tolerance or offset change). detail: the summary counts.

Content structure

NameDescription
(sources)Child <ol data-source> / <ul data-source> lists of <li> entries. Entry content (inline markup such as <code> or links) is cloned into the lanes; ids are stripped from the clones.

CSS classes

NameDescription
mv-accounts-bar / -views / -view / -tool / -pickToolbar: view radio group, witness picker, “Only disagreements” and Clocks toggles.
mv-accounts-clocks / -clock-row / -offsetClock correction panel and its per-source offset inputs.
mv-accounts-summary / -details / -jumps / -jumpSummary line with counts and the list of disagreements as jump links.
mv-accounts-grid / -lane / -head / -listLanes layout (CSS grid with subgrid lanes so rows align across sources). The fact gutter is the lane with data-role="facts"; data-focused / data-dimmed in witness view.
mv-accounts-entry / -fact / -link / -verdictEntry cards (data-status: agree | dissent | split | single | silent, data-kind: time | value), gutter facts, links across lanes and the verdict row. data-hot / data-linked mark every piece of the hovered or focused fact.
mv-accounts-merged / -card / -claims / -claim / -consensus / -dotMerged view: fact cards on a rail, their claims per source and the consensus meter (circle = agrees, square = contradicts, dashed = silent).
mv-accounts-glyphStatus shapes: check disc (agree), crossed diamond (conflict), half disc (single source), dashed ring (silent).

CSS variables

NameDefaultDescription
--mv-accounts-agreevar(--mv-success)Agreement tone.
--mv-accounts-conflictvar(--mv-danger)Conflict tone.
--mv-accounts-singlevar(--mv-warning)Tone of facts only one source mentions.
--mv-accounts-silentvar(--mv-fg-subtle)Tone of silent cells.
--mv-accounts-gutter12.5remWidth of the fact gutter (9.5rem in narrow containers).
--mv-accounts-lane-min12.5remMinimum lane width; below it the lanes scroll horizontally with the gutter sticky.
--mv-accounts-max-heightnoneCaps the height of the lanes / merged list; the lane headers then stay sticky while scrolling.

Accessibility

Every lane is a <section> with a real heading (level set by heading-level) and an ordered list, the fact gutter included, so screen reader users can browse each account by heading and list. Each entry is a list item that names its fact and ends with a full verdict sentence (“Places it 4 min later than API gateway and Mobile app (2:02:31 PM).”, “Silent here; API gateway and Mobile app mention it.”, the reported time and correction when a clock was adjusted); the short visible labels (Agrees, +4 min, Contradicts, Only source, Silent) are paired with distinct shapes (check disc, crossed diamond, half disc, dashed ring) and never rely on color. The body is one tab stop with a roving tabindex: arrows move within a lane and across lanes on the same fact row, Alt+Left/Right jumps to the same fact in the next source that mentions it (skipping silent ones), Alt+Up/Down to the previous or next disagreement, Home/End and Ctrl+Home/End to the ends, Enter or Space selects a fact and Escape clears it; these instructions are exposed as the group's description. Hovering or focusing any piece of a fact highlights its entries and link in every lane. The view switcher is a radio group (arrow keys), the filter a toggle button (aria-pressed), Clocks a disclosure (aria-expanded) whose offset inputs are labelled per source, accept durations like -2m40s and step with Up/Down (1 s, Shift 1 min) and PageUp/PageDown. The summary is a native <details> whose items are buttons that move focus to the disagreement. View changes, selections (with their verdict) and clock corrections (with the new summary) are announced in a polite live region; focus is kept on the same fact across re-renders. Reduced motion (OS or data-motion="reduce"): no FLIP reordering, no status flash, instant scrolling. Forced colors: shapes use CanvasText, selection and active controls use Highlight outlines, conflicts get a thicker border.