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.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-accounts>) |
| Status | beta |
| Keywords | exclusive, 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
- An incident post-mortem must reconcile logs, vendor reports, client telemetry and human notes into one agreed timeline
- A support or claims team compares a customer's story with carrier and warehouse records to settle a dispute
- Several sources report the same events with clocks that drift, and the skew must be corrected before judging who is right
- Fact-checkers or auditors need to see at a glance which claims are corroborated, contradicted or backed by a single source
Avoid when
- The documents are static forms or records compared field by field rather than timestamped narratives → use Papers instead
- There is a single source of events: a plain chronological list or activity feed is clearer → use Animated List instead
- Thousands of events must be reconciled in bulk; this is a review surface for tens of facts, use a data table with a match column → use Data table instead
Install
node scripts/add.mjs accounts --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
view | lanes | merged | witness | lanes | lanes: 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. |
witness | source id | Source focused by the witness view (default: the first source). | |
tolerance | duration | 1m | Times 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). |
reference | source id | System 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). | |
selected | fact key | Selected fact: highlighted in every view, with a written verdict under its row in the lanes views. Reflects user selection. | |
filter | all | issues | all | issues hides facts on which every source agrees (the “Only disagreements” toggle). |
locale | BCP 47 | en-US | Locale for times, durations, plurals and name lists. |
time-zone | IANA zone | Zone used to display times (default: the viewer's; UTC when every time is wall-clock only). Shown in the gutter header. | |
precision | auto | seconds | minutes | auto | Time display precision; auto shows seconds when any entry has them. |
label | string | Accounts | Accessible name of the timeline group. |
heading-level | 1–6 | 3 | Level of the lane headings. |
controls | "true" | "false" | true | "false" hides the built-in toolbar (views, filter, Clocks) when the app drives them. |
data-source | string | On 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-description | string / duration / string | On 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-at | ISO 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-key | string | On an entry: the fact it claims. Same key across sources = same fact. Entries without a key are facts only their source mentions. | |
data-value | string | On 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-tolerance | string / duration | "any" | On an entry: the fact's title (first one found wins, default: the key humanized) and a per-fact tolerance. |
Properties
| Name | Type | Description |
|---|---|---|
sources | Array<{ 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. |
facts | Array<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). |
offsets | Record<sourceId, ms> | Current clock corrections (read-only; use setOffset). |
factLabels | Record<key, { label?, tolerance? }> | Titles and tolerances per fact, when the markup cannot carry them. |
compare | (a: string, b: string, key: string) => boolean | Custom value equality (numeric tolerance, synonyms…). Default: case-, accent- and whitespace-insensitive. |
strings | Partial<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 / controls | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-select-fact | Cancelable, 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-change | Cancelable, before the view changes. detail: { view, previous, witness }. |
mv-offset-change | Cancelable, before a clock correction is applied. detail: { source, offset, previous, reason: "input" | "align" | "reset" | "api" }. |
mv-reconcile | After every analysis (data, tolerance or offset change). detail: the summary counts. |
Content structure
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-accounts-bar / -views / -view / -tool / -pick | Toolbar: view radio group, witness picker, “Only disagreements” and Clocks toggles. |
mv-accounts-clocks / -clock-row / -offset | Clock correction panel and its per-source offset inputs. |
mv-accounts-summary / -details / -jumps / -jump | Summary line with counts and the list of disagreements as jump links. |
mv-accounts-grid / -lane / -head / -list | Lanes 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 / -verdict | Entry 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 / -dot | Merged view: fact cards on a rail, their claims per source and the consensus meter (circle = agrees, square = contradicts, dashed = silent). |
mv-accounts-glyph | Status shapes: check disc (agree), crossed diamond (conflict), half disc (single source), dashed ring (silent). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-accounts-agree | var(--mv-success) | Agreement tone. |
--mv-accounts-conflict | var(--mv-danger) | Conflict tone. |
--mv-accounts-single | var(--mv-warning) | Tone of facts only one source mentions. |
--mv-accounts-silent | var(--mv-fg-subtle) | Tone of silent cells. |
--mv-accounts-gutter | 12.5rem | Width of the fact gutter (9.5rem in narrow containers). |
--mv-accounts-lane-min | 12.5rem | Minimum lane width; below it the lanes scroll horizontally with the gutter sticky. |
--mv-accounts-max-height | none | Caps 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.