Exclusivebeta

Certainty — <mv-certainty>

Text whose passages each carry a confidence level (AI answer, summary, report), rendered calmly so readers can calibrate their trust without a wall of warnings: three tiers (Verified with no mark, Likely with a dotted underline, Uncertain with a hand-drawn wave whose amplitude grows with doubt) and ink that lightens as confidence drops. Hovering or focusing a passage opens a lens (percentage, tier, gauge, reason, sources, "Flag") and slightly dims the rest; an "Only show what's at least X% reliable" slider blurs or collapses passages below the threshold; a strip sums up overall reliability. Works with existing markup or with segments appended as a stream arrives.

CategoryAnimated text
TypeWeb Component (<mv-certainty>)
Statusbeta
Keywordsexclusive, text, ai, llm, confidence, uncertainty, trust, calibration, citations, sources, hallucination, streaming, annotation, threshold

When to use

Avoid when

Install

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

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

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/position.js, components/certainty/certainty.js, components/certainty/certainty.css.

Usage

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

<div id="ct-demo" style="width:min(100%,46rem);margin-inline:auto">
  <style>
    #ct-demo { display:grid; gap:2.25rem }
    #ct-demo .ct-card { display:grid; gap:1rem; padding:1.25rem 1.375rem 1.375rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #ct-demo .ct-ask { justify-self:end; max-width:85%; margin:0; padding:.55rem .9rem; border-radius:1rem 1rem .25rem 1rem; background:var(--mv-bg-muted); font-size:.9375rem; line-height:1.45 }
    #ct-demo .ct-who { display:flex; align-items:center; gap:.5rem; margin:0; color:var(--mv-fg-muted); font-size:.75rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #ct-demo .ct-who::before { content:""; width:.5rem; height:.5rem; border-radius:50%; background:var(--mv-accent) }
    #ct-demo mv-certainty { font-size:1rem }
    #ct-demo .ct-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding-top:.25rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #ct-demo .ct-foot .mv-choice { font-size:.8125rem }
    #ct-demo .ct-head { display:flex; align-items:baseline; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin:0 0 -.25rem }
    #ct-demo .ct-head h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
    #ct-demo .ct-head span { color:var(--mv-fg-muted); font-size:.8125rem }
    #ct-demo .ct-flagged { font-variant-numeric:tabular-nums }
  </style>

  <!-- 1 · Annotated markup: summary strip + threshold -->
  <section class="ct-card" aria-label="Annotated answer">
    <p class="ct-ask">Is a heat pump worth it for my house built in 1975?</p>
    <p class="ct-who">Assistant</p>
    <mv-certainty id="ct-answer" summary filter mode="dim" label="Answer reliability">
      <p>For a house built in 1975, <span data-confidence="0.93" data-reason="Standard retrofit for forced-air homes of that era." data-sources="U.S. Department of Energy — Heat pump systems|https://www.energy.gov/energysaver/heat-pump-systems">an air-source heat pump can usually replace both the furnace and the central air conditioner</span>. <span data-confidence="0.66" data-reason="Inferred from the construction year; only an energy audit will confirm it.">Homes from that period often have thin wall insulation and leaky ductwork</span>.</p>
      <p><span data-confidence="0.95" data-reason="Well established, confirmed by two official sources." data-sources="U.S. Department of Energy — Heat pump systems|https://www.energy.gov/energysaver/heat-pump-systems;ENERGY STAR — Heat pumps|https://www.energystar.gov/">Modern heat pumps work efficiently in most climates, and cold-climate models keep heating well below freezing</span>. <span data-confidence="0.34" data-reason="Based on 2023 contractor quotes; current regional prices were not found.">A ducted whole-house system would cost about $14,000 to $20,000 installed</span>.</p>
      <p><span data-confidence="0.9" data-reason="Certification criteria are published and stable." data-sources="ENERGY STAR — Heat pumps|https://www.energystar.gov/">ENERGY STAR certified models use noticeably less electricity than standard ones</span>, and <span data-confidence="0.97" data-reason="Basic physics of heat transfer, explained by the DOE." data-sources="U.S. Department of Energy — Heat pump systems|https://www.energy.gov/energysaver/heat-pump-systems">they move heat rather than generate it, which is why they can deliver more energy than they consume</span>. <span data-confidence="0.58" data-reason="Depends on your utility and local program.">Many utilities offer rebates for qualifying installations</span>, <span data-confidence="0.18" data-reason="Federal credit rules changed recently; no current source confirms this.">and these would still stack with federal tax credits</span>.</p>
      <p><span data-confidence="0.72" data-reason="Common in many states, not everywhere." data-sources="DSIRE — Database of State Incentives|https://www.dsireusa.org/">Your state or city may add its own incentive</span>: <span data-confidence="0.41" data-reason="Generalized from a handful of programs only.">it often takes the form of a $500 to $1,500 rebate</span>. <span data-confidence="0.88" data-sources="U.S. Department of Energy — Home energy audits|https://www.energy.gov/energysaver/home-energy-audits">The best first step is a professional home energy audit</span>, which will confirm current prices and programs.</p>
    </mv-certainty>
    <div class="ct-foot">
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ct-collapse"><span class="mv-choice-text"><span class="mv-choice-title">Collapse hidden passages</span></span></label>
      <span class="ct-flagged" id="ct-flagged" aria-live="polite">Hover or tab to a passage to see where it comes from.</span>
    </div>
  </section>

  <!-- 2 · Streaming: segments appended as they arrive -->
  <section class="ct-card" aria-label="Streamed answer">
    <div class="ct-head"><h3>Live stream</h3><span>segments added one by one with append()</span></div>
    <p class="ct-ask">When did the Musée d'Orsay open?</p>
    <mv-certainty id="ct-stream" summary label="Reliability of the answer in progress"></mv-certainty>
    <div class="ct-foot">
      <span id="ct-stream-state">Generating…</span>
      <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ct-regen">Regenerate</button>
    </div>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-certainty");
    const answer = document.getElementById("ct-answer");
    const flagged = document.getElementById("ct-flagged");
    document.getElementById("ct-collapse").addEventListener("change", (e) => { answer.mode = e.target.checked ? "collapse" : "dim"; });
    answer.addEventListener("mv-flag", () => {
      const n = answer.stats.flagged;
      flagged.textContent = n ? `${n} passage${n > 1 ? "s" : ""} flagged for review` : "No passages flagged";
    });

    // Streaming: two alternative answers, each segment arrives on its own.
    const orsay = "Musée d'Orsay — Official website|https://www.musee-orsay.fr/en";
    const takes = [
      [
        { text: "The Musée d'Orsay opened its doors " },
        { text: "in December 1986", confidence: 0.94, reason: "Date given by the museum's official website.", sources: orsay },
        { text: ", inside the former Gare d'Orsay, " },
        { text: "a railway station built for the 1900 World's Fair", confidence: 0.9, reason: "Several sources agree.", sources: orsay },
        { text: ". " },
        { text: "Its interior was designed by Gae Aulenti", confidence: 0.74, reason: "She designed the interior alongside ACT Architecture, who led the conversion." },
        { text: ", and " },
        { text: "the museum reportedly welcomes nearly 4 million visitors a year", confidence: 0.38, reason: "Attendance varies from year to year; the figure is undated." },
        { text: "." },
      ],
      [
        { text: "Housed in a station " },
        { text: "designed by Victor Laloux", confidence: 0.87, reason: "Architect named by every source consulted.", sources: orsay },
        { text: ", the Musée d'Orsay " },
        { text: "was inaugurated in late 1986", confidence: 0.92, sources: orsay },
        { text: ".\n\n" },
        { text: "Its collection mostly spans 1848 to 1914", confidence: 0.69, reason: "Usual boundaries, sometimes presented differently." },
        { text: ", and " },
        { text: "attendance reportedly neared 3.9 million visitors in 2023", confidence: 0.46, reason: "Press figure, not checked against the annual report." },
        { text: "." },
      ],
    ];
    const stream = document.getElementById("ct-stream");
    const state = document.getElementById("ct-stream-state");
    let run = 0;
    let take = 0;
    async function play() {
      const id = ++run;
      stream.segments = [];
      stream.setAttribute("aria-busy", "true");
      state.textContent = "Generating…";
      for (const seg of takes[take]) {
        await new Promise((r) => setTimeout(r, seg.confidence === undefined ? 160 : 520));
        if (id !== run || !stream.isConnected) return;
        stream.append(seg);
      }
      stream.setAttribute("aria-busy", "false");
      state.textContent = `Done · ${stream.stats.count} annotated passages`;
    }
    document.getElementById("ct-regen").addEventListener("click", () => { take = (take + 1) % takes.length; play(); });
    play();
  </script>
</div>

API

Attributes

NameTypeDefaultDescription
summarybooleanShows the "Overall reliability" strip: mean confidence weighted by passage length, a segmented bar of tier proportions and a legend of the three underline styles.
filterbooleanShows the "Only show what's at least X% reliable" threshold slider and the count of passages below the threshold.
thresholdnumber (0..1)0Confidence threshold: passages below it recede (0 = show everything). Also accepts 0..100.
modedim | collapsedimdim: passages below the threshold stay in place, blurred and faded (stable layout; hover or focus to read them). collapse: they leave the flow and are replaced by a "… uncertain passage hidden" pill that reveals them on click.
tiersstring0.5,0.8Tier boundaries: below the first = Uncertain, from the second up = Verified, in between = Likely.
labelstringText reliabilityAccessible name of the strip.
aria-busy"true"While streaming: shows a subtle typing caret at the end of the generated text.
data-confidencenumber (0..1 or %)On a child element (span…): makes it an annotated passage. Passages added or changed later are picked up.
data-reasonstringOn a passage: short justification shown in the lens ("single source, dated 2023").
data-sourcesstringOn a passage: "Title|https://…;Title 2|https://…". Only http(s) URLs become links, opened in a new tab.

Properties

NameTypeDescription
segments{ text, confidence?, reason?, sources? }[]Text rendered with text nodes only (never HTML). Without confidence: neutral text; a blank line (\n\n) starts a paragraph. sources: an array of { title, url } or a string in the data-sources format. Assigning replaces all generated content.
statsobjectRead-only: { count, confidence (weighted mean), threshold, hidden, flagged, tiers: { verified | likely | uncertain: { label, count, share } }, passages: [...] }.

Methods

NameDescription
append(segment | segment[])Appends one or more segments at the end (streamed LLM output) with a fade-in; returns the created passages. Given DOM nodes, behaves like Element#append().
setThreshold(value)Sets the threshold (0..1 or 0..100); same as the threshold attribute.
inspect(index) / close()Opens the lens on passage number index / closes it.

Events

NameDescription
mv-thresholdThe threshold changed. detail: { threshold, hidden, count }.
mv-inspectThe lens opens on a passage. detail: { index, text, confidence, tier, label, reason, sources, below, flagged, element }.
mv-flag"Flag" (toggle). detail: the passage data, including flagged. The passage gets data-flagged.

Content structure

NameDescription
(content)Any text (paragraphs, lists…) containing data-confidence elements. With segments, text is rendered into a [data-certainty-body] container (provided or created).

CSS classes

NameDescription
mv-certainty-passageAnnotated passage (data-tier = verified | likely | uncertain, data-below under the threshold, data-active under the lens).
mv-certainty-bar / -segments / -legend / -rangeSummary strip, segmented bar, legend and threshold slider.
mv-certainty-lensLens (popover): tier, percentage, gauge, reason, sources, .mv-certainty-flag button.
mv-certainty-stubPill standing in for a collapsed passage (collapse mode).

CSS variables

NameDefaultDescription
--mv-certainty-huevar(--mv-accent)Single hue for the whole scale (underlines, bar, gauge), stepped light to dark.
--mv-certainty-inkvar(--mv-fg)Ink of verified text.
--mv-certainty-ink-likely / -ink-uncertain84% / 68%Ink density of likely / uncertain passages.
--mv-certainty-leading1.8Line height (leaves room for the waves).
--mv-certainty-bar-bgSummary strip background.

Accessibility

Confidence is never conveyed by color alone: ink density + underline style (none / dotted / wavy) + a text label (lens, legend, description). Every passage is focusable (Tab) and described via aria-describedby ("42% confidence, uncertain: reason. 2 sources."); on keyboard focus the lens opens without stealing focus, Enter or Space moves into it (links, "Flag"), Tab out of it resumes reading right after the passage, Shift+Tab returns to the passage, Escape closes it and restores focus. The lens is a non-modal role="dialog" (manual popover), also closed by an outside click; on touch, a tap opens or closes it. The threshold slider is a labelled native range input (aria-valuetext "60% reliable"); the number of passages below the threshold is announced politely after a pause (role=status). Documented choice for hidden passages: in dim mode they stay in the accessibility tree with "Below the threshold" in their description; in collapse mode they leave it (display: none) and the pill, a real button, announces them ("Hidden uncertain passage, 32% confidence. Show"). prefers-reduced-motion and [data-motion="reduce"]: instant transitions, no fade-in or blinking caret.