Exclusivebeta

Trio Check — <mv-trio-check>

Batch verification for quizzes, knowledge checks and matching exercises that makes guessing useless: answers are entered freely (your own inputs, selects, radios or checkboxes marked data-trio-item, or rows rendered from an items property with text fields, selects and choice chips), and the component never reveals whether a single answer is right. Only when a whole set of answers (3 by default, batch) is correct at once is it locked in: the rows turn read-only and get a stamped seal with “Confirmed”, the meter fills a set, and the rest stays ambiguous (4 right answers lock 3; the last set shrinks to what is left). A failed attempt only says “Not enough correct yet. Nothing was locked.” with a small shake of the meter, an identical resubmission is refused without spending an attempt, and an optional cooldown drains the Verify button before the next try. Correctness comes from an async check(answers) callback (your server, which can apply the same rule and return only the ids it confirms) or from salted SHA-256 hashes in data-answer-hash (with a static hash() helper and case, space and accent folding), so answers never ship to the browser in clear. A sticky bar shows “6 of 12 confirmed · 2 answered”, verification can run automatically on change, progress can be restored from the server, and a completion state (big seal, attempt count, your own continue button) closes the exercise.

CategoryForms
TypeWeb Component (<mv-trio-check>)
Statusbeta
Also installsbutton, input, select
Keywordsexclusive, culture, quiz, assessment, knowledge-check, compliance-training, onboarding, matching, batch-verification, anti-guessing, language-learning, puzzle, form, answers, hash, cooldown, progress, seal

When to use

Avoid when

Install

node scripts/add.mjs trio-check --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["trio-check"], 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/motion.js, components/trio-check/trio-check.js, components/trio-check/trio-check.css, components/button/button.css, components/input/input.css, components/select/select.css.

Usage

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

<div id="tc-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #tc-demo { display:grid; gap:1.5rem }
    #tc-demo .tc-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #tc-demo .tc-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1.125rem 1.25rem 0 }
    #tc-demo .tc-head h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #tc-demo .tc-head p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.45 }
    #tc-demo .tc-body { display:grid; grid-template-columns:17.5rem minmax(0,1fr); gap:1.25rem; align-items:start; padding:1.125rem 1.25rem 1.25rem }
    #tc-demo .tc-policy { position:sticky; top:1rem; padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); font-size:.8125rem; line-height:1.5 }
    #tc-demo .tc-policy h4 { display:flex; align-items:center; gap:.5rem; margin:0 0 .125rem; font-size:.8125rem }
    #tc-demo .tc-policy h4 svg { width:1rem; height:1rem; color:var(--mv-fg-muted) }
    #tc-demo .tc-policy > p { margin:0 0 .75rem; color:var(--mv-fg-subtle); font-size:.75rem }
    #tc-demo .tc-policy ol { display:grid; gap:.625rem; margin:0; padding:0 0 0 1.125rem }
    #tc-demo .tc-policy li::marker { color:var(--mv-fg-subtle); font-variant-numeric:tabular-nums }
    #tc-demo .tc-policy b { font-weight:600 }
    #tc-demo .tc-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle); border-radius:0 0 var(--mv-radius-xl) var(--mv-radius-xl) }
    #tc-demo .tc-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); flex:1 1 18rem }
    #tc-demo .tc-actions { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
    #tc-demo .mv-choice { font-size:.8125rem }
    #tc-demo .tc-lang { padding:1.125rem 1.25rem 1.25rem; display:grid; gap:1rem }
    #tc-demo .tc-lang .tc-head { padding:0 }
    @media (max-width:52rem) {
      #tc-demo .tc-body { grid-template-columns:minmax(0,1fr) }
      #tc-demo .tc-policy { position:static }
    }
  </style>

  <!-- Compliance training: a matching exercise checked by a (fake) server that only knows salted hashes -->
  <section class="tc-card" aria-labelledby="tc-desk-title">
    <div class="tc-head">
      <div>
        <h3 id="tc-desk-title">Refund desk certification</h3>
        <p>Pick the right action for each ticket using only the handbook. Guessing one at a time won’t help.</p>
      </div>
      <span class="mv-badge" data-variant="secondary" data-shape="pill">Module 2 of 4</span>
    </div>
    <div class="tc-body">
      <aside class="tc-policy" aria-labelledby="tc-policy-title">
        <h4 id="tc-policy-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h12v18H6a2 2 0 0 1-2-2z"/><path d="M8 7h6M8 11h6"/></svg>Returns handbook</h4>
        <p>Northwind Outfitters · Support policy v3.2</p>
        <ol>
          <li>Unworn items returned <b>within 30 days</b> of delivery: refund to the original payment method.</li>
          <li>Returns between <b>31 and 60 days</b>: store credit only.</li>
          <li>After <b>60 days</b>, or any <b>final-sale</b> item: decline.</li>
          <li>Damaged or wrong item <b>with a photo</b>: send a free replacement, no return needed.</li>
          <li>Any refund <b>over $500</b> goes to a team lead first. This overrides rules 1 and 2.</li>
        </ol>
      </aside>

      <mv-trio-check id="tc-desk" label="Refund tickets">
        <ol class="mv-trio-check-list">
          <li class="mv-trio-check-item" data-trio-item="t4127" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">1</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4127">Unworn trail shoes, delivered 12 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4127-h">T-4127 · Priya Nair · $129.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4127" aria-describedby="tc-t4127-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund" selected>Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4131" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">2</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4131">Jacket with tags on, delivered 45 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4131-h">T-4131 · Lucas Moreau · $210.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4131" aria-describedby="tc-t4131-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit" selected>Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4138" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">3</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4138">Tent pole snapped in the box, photo attached</label>
              <span class="mv-trio-check-hint" id="tc-t4138-h">T-4138 · Aiko Tanaka · $340.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4138" aria-describedby="tc-t4138-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace" selected>Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4142">
            <span class="mv-trio-check-num" aria-hidden="true">4</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4142">Two unworn parkas, delivered 9 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4142-h">T-4142 · Mateo García · $1,180.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4142" aria-describedby="tc-t4142-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate" selected>Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4150">
            <span class="mv-trio-check-num" aria-hidden="true">5</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4150">Final-sale swimsuit, never worn</label>
              <span class="mv-trio-check-hint" id="tc-t4150-h">T-4150 · Amara Okafor · $48.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4150" aria-describedby="tc-t4150-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit" selected>Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4153">
            <span class="mv-trio-check-num" aria-hidden="true">6</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4153">Received size M instead of L, photo attached</label>
              <span class="mv-trio-check-hint" id="tc-t4153-h">T-4153 · Jonas Lindqvist · $95.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4153" aria-describedby="tc-t4153-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4159">
            <span class="mv-trio-check-num" aria-hidden="true">7</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4159">Unworn backpack, delivered 75 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4159-h">T-4159 · Sofia Rossi · $160.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4159" aria-describedby="tc-t4159-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4164">
            <span class="mv-trio-check-num" aria-hidden="true">8</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4164">Unused camp stove set, delivered 40 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4164-h">T-4164 · Daniel Kim · $620.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4164" aria-describedby="tc-t4164-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4170">
            <span class="mv-trio-check-num" aria-hidden="true">9</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4170">Gloves that did not fit, delivered 3 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4170-h">T-4170 · Fatima Zahra · $35.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4170" aria-describedby="tc-t4170-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
        </ol>
        <button slot="complete" type="button" class="mv-button" data-size="sm">Continue to module 3</button>
      </mv-trio-check>
    </div>
    <div class="tc-foot">
      <p class="tc-log" id="tc-log">Progress restored: 3 tickets confirmed last session.</p>
      <span class="tc-actions">
        <label class="mv-choice">
          <input type="checkbox" role="switch" class="mv-switch" id="tc-auto">
          <span class="mv-choice-text"><span class="mv-choice-title">Verify automatically</span></span>
        </label>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tc-restart">Restart module</button>
      </span>
    </div>
  </section>

  <!-- Language exercise: items rendered from the items property, hashes checked in the browser, 5 s cooldown -->
  <section class="tc-card tc-lang" aria-labelledby="tc-lang-title">
    <div class="tc-head">
      <div>
        <h3 id="tc-lang-title">Spanish A2 · Around the house</h3>
        <p>Type the Spanish word or pick the article. Accents are optional. A wrong set means a 5-second pause.</p>
      </div>
      <span class="mv-badge" data-variant="outline" data-shape="pill">Unit 4 review</span>
    </div>
    <mv-trio-check id="tc-lang" cooldown="5s" salt="es-a2-unit4" label="Spanish vocabulary"></mv-trio-check>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-trio-check");
    const TrioCheck = customElements.get("mv-trio-check");
    const desk = document.getElementById("tc-desk");
    const lang = document.getElementById("tc-lang");
    const log = document.getElementById("tc-log");
    const say = (text) => { log.textContent = text; };

    // Stand-in for your server. It only stores salted hashes (never the answers)
    // and applies the batch rule itself, so the browser learns nothing beyond
    // the ids it confirms.
    const SALT = "nw-refunds-2026";
    const SERVER = {
      t4127: "a3ba7943f2e72ef6b5ca475382d0eca784d445574ede4717e96a36b4a7588c4e",
      t4131: "e7cc294f4a77a7d1c10c4142739cda2431b2b6869a4ef6684dda97e971e86a01",
      t4138: "d922dfd2e3fd4310170c1fd92679c589ebb0499e6f737b1a203acc0240db3dd2",
      t4142: "d2fc233747364ef0232c0f5d1fdba7cdaea97e82d01ecc94bf3444b7331eb0e9",
      t4150: "a6776de3017d2432c3d1bf6b0a9b8afcdac2c5791f720a924be4e046cac1816a",
      t4153: "b0149df55dbf622aeedffad0d4e62f3721d3924321aef4fbc765d5150ae48448",
      t4159: "d4af7ffa16863311616fcddf768dbd75c2338916943075eb4743fe2e6cedf129",
      t4164: "3eb3c92353866407a49c892d0eff0ce98964ead87c5d92366a0acc6d3b03b00d",
      t4170: "fac735b4ff9b86b4883c067b073aebed49ec0e7740641933b722a296fae220e3",
    };
    const fakeServer = async (answers) => {
      await new Promise((r) => setTimeout(r, 700));
      const ids = Object.keys(answers);
      const size = Math.min(3, ids.length);
      const right = [];
      for (const id of ids) {
        if (answers[id] && (await TrioCheck.hash(answers[id], { id, salt: SALT })) === SERVER[id]) right.push(id);
      }
      return right.slice(0, Math.floor(right.length / size) * size);
    };
    desk.check = fakeServer;

    desk.addEventListener("mv-attempt", (e) => say(`Attempt ${e.detail.attempt}: sending ${Object.values(e.detail.answers).filter(Boolean).length} answers…`));
    desk.addEventListener("mv-miss", (e) => say(`Attempt ${e.detail.attempt}: no set of three was fully right.`));
    desk.addEventListener("mv-confirm", (e) => { if (e.detail.source !== "api") say(`Attempt ${e.detail.attempt}: locked ${e.detail.ids.map((id) => id.replace("t", "T-")).join(", ")}.`); });
    desk.addEventListener("mv-complete", (e) => say(`Module complete: ${e.detail.total} tickets in ${e.detail.attempts} attempt${e.detail.attempts === 1 ? "" : "s"}.`));
    document.getElementById("tc-auto").addEventListener("change", (e) => { desk.auto = e.target.checked; });
    document.getElementById("tc-restart").addEventListener("click", () => { desk.reset(); say("Module restarted."); });

    // Rendered items. answerHash = TrioCheck.hash(answer, { id, salt }), computed at authoring time.
    const articles = [{ value: "el", label: "el" }, { value: "la", label: "la" }];
    lang.items = [
      { id: "kitchen", prompt: "the kitchen", hint: "Where you cook", placeholder: "Spanish word", value: "cocina",
        answerHash: ["a4896e33902ad389f472740db007eea22e43d590fc1f4c5c430c9dd6970b7724", "fb069ef1f4fa3413edaabc7a72a7a30c8735520f0f14132edf5f043ceb09fbda"] },
      { id: "window", prompt: "the window", hint: "You open it for fresh air", placeholder: "Spanish word",
        answerHash: ["c31ccf4196fdeaf60478cf4e644eb11fea7de36ed9429f44a837335499f62cb1", "337ffcd8d39756b9286b0007c5ac8dc15c8e73ed6c2964a1083ed50724a76e6e"] },
      { id: "key", prompt: "the key", hint: "It opens the front door", placeholder: "Spanish word",
        answerHash: ["280dbdb70b411e2b86127ec29b7dd70b69ad984b63cfa4342e3c206166bea1e7", "d75dcae5ca5e689048bbec201542f2961c9c46d72d194585d69198881e618649"] },
      { id: "mesa", type: "choice", prompt: "___ mesa", hint: "table", options: articles, value: "la",
        answerHash: "ac606155a337df73bd53b2ddd39beefa03f47acc984a571f81b7f9d38fa56310" },
      { id: "sofa", type: "choice", prompt: "___ sofá", hint: "sofa", options: articles,
        answerHash: "f6c260d5de282870e993113b9723a561b7aeb4250716f78fc1ddd976dd7435f5" },
      { id: "mano", type: "choice", prompt: "___ mano", hint: "hand", options: articles,
        answerHash: "bd9744211255e86a44adea7ad774e131c567e1b15a9c4c33cd6523d7b247c4e7" },
    ];
  </script>
</div>

Cultural reference

Return of the Obra Dinn — Lucas Pope (2018, game). The insurance ledger only confirms your deductions in batches: fates are locked in as correct only when three are right at once, so they can’t be brute-forced one by one. In the UI, answers are only ever confirmed in sets of three (or any batch size), sealed together, while a failed attempt reveals nothing about which answer was wrong.

API

Attributes

NameTypeDefaultDescription
batchnumber3How many answers must be right at once to be locked in. Correct answers lock in whole sets only (floor(correct / batch) × batch, in document order); when fewer items than batch are left open, the last set is whatever remains. 1 turns it into plain per-answer verification.
cooldowntime ("5s", "800ms", "1m", ms number)Pause imposed after an attempt that locked nothing: the Verify button reads “Try again in 4s”, drains back and stays aria-disabled. No cooldown by default.
autobooleanVerify on its own 650 ms after an answer is committed (change event: select picked, radio chosen, text field left), whenever enough answers are filled and no cooldown runs. Identical answer sets are never resubmitted.
saltstringSalt mixed into data-answer-hash hashes (see hash()). Ignored when check is set.
normalizefold | trim | exactfoldHow answers are normalized before hashing: fold trims, collapses spaces, lowercases and strips accents (“La Cocína ” = “la cocina”); trim only trims and collapses spaces; exact hashes the raw value. check() always receives raw values.
labelstringKnowledge checkAccessible name of the group (aria-label on the host, unless it has aria-labelledby).
data-trio-itemstring (item id)Marks an item. Its value is the id used in answers, events and hashes; when empty, the element id or a generated id is written back.
data-answer-hashhex SHA-256, space or comma separatedOn an item: accepted answer hashes (several for synonyms such as “cocina” and “la cocina”). Used when no check callback is set.
data-trio-fieldmarker attributeInside an item: the control whose value property is the answer (a custom element, a combobox…). Otherwise the first text field, textarea or select, the checked radio, or the sorted checked checkbox values joined with commas.
data-trio-valuestringOn an item: the answer itself, set by your code for custom widgets (drag and drop, maps, pickers). Takes precedence over any field.
data-trio-statusmarker attributeInside an item: where the confirmed status (seal + “Confirmed”) is written. Otherwise a .mv-trio-check-status span is appended to the item.
data-trio-stateempty | answered | confirmedSet by the component on each item. Server-rendered items with data-trio-state="confirmed" are locked silently on start (restored progress). data-trio-batch holds the set number of a confirmed item.
data-stateidle | ready | pending | cooldown | completeSet on the host: not enough answers yet, verifiable, checking, cooling down, all confirmed. aria-busy is set while checking.

Properties

NameTypeDescription
check(answers: Record<id, string>) => Promise<string[] | Set<string> | Record<id, boolean>>Your verifier. Receives every open (unconfirmed) item, empty string when unanswered, and resolves to the ids that are correct. For tamper-proof assessments, apply the same batch rule on the server and return only the ids you confirm: the component’s rule is idempotent. A rejection shows “Your answers couldn’t be checked” and spends no lock.
itemsArray<{ id, prompt, hint?, type?: "text" | "select" | "choice", options?: (string | { value, label })[], placeholder?, value?, answerHash?: string | string[] }>Rows rendered by the component into an <ol class="mv-trio-check-list"> before the bar: labelled text field (mv-input), select (mv-select, with a disabled placeholder) or a radio group of choice chips. type defaults to select when options are given, else text.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (keys: label, verify, cooldown ({s}), progress ({confirmed}, {total}), answered ({n}), rule ({batch}), ruleOne, needMore ({n}), miss, missCooldown ({s}), repeat, confirmed ({n}), confirmedOne, rest, ready, error, complete ({total}), completeText ({n}), completeTextOne, announceComplete ({total}, {n}), status, statusSr ({batch}), meter, choose). English defaults.
answersRecord<id, string>Current value of every item, confirmed or not (read-only).
confirmedstring[]Ids of the confirmed items in document order (read-only).
progress{ confirmed, total, answered, attempts, complete }Counts shown in the bar; answered counts open items with a value (read-only).
attempts / pending / coolingnumber / boolean / number (ms)Attempts spent, whether a check is in flight, time left in the cooldown (read-only).

Methods

NameDescription
verify({ reason? })Submits the open answers like the Verify button (reason defaults to "api"). Resolves to the ids locked by this attempt, [] when nothing was locked or the attempt was refused (not enough answers, cooldown, pending, identical to the last failed set, mv-attempt cancelled).
MvTrioCheck.hash(answer, { id, salt, normalize })Static: resolves to the hex SHA-256 of ${salt}:${id}:${normalized answer}, the format data-answer-hash expects. Run it at authoring time or on your server; requires a secure context (HTTPS or localhost).
confirm(ids, { animate? })Locks items without checking, e.g. progress restored from your server. Emits mv-confirm with source "api" (and mv-complete if everything is confirmed). Silent unless animate is true.
reset({ keepAnswers? })Unlocks every item, restores each control’s own readonly / disabled state, clears the answers (unless keepAnswers), the attempts, the cooldown and the feedback.

Events

NameDescription
mv-attemptCancelable, before an attempt is sent. detail: { attempt, answers (open items only), batch (set size for this attempt), reason: "button" | "keyboard" | "auto" | "api" }. preventDefault() refuses it without spending an attempt (e.g. a daily limit).
mv-confirmItems were locked. detail: { ids, items (elements), attempt, source (the attempt reason, or "api"), confirmed, total }.
mv-missAn attempt locked nothing. detail: { attempt, answered, cooldown (ms, 0 when none) }. Never says which answers were wrong.
mv-completeEvery item is confirmed. detail: { attempts, total, duration (ms since the first attempt) }.
mv-check-errorThe check callback rejected, or hashing failed (insecure context). detail: { attempt, error }.

Content structure

NameDescription
(content)Your items: any structure with [data-trio-item] elements (use .mv-trio-check-list / .mv-trio-check-item for the default row layout), or nothing when you use the items property. The bar is appended last.
completeChildren with slot="complete" (a Continue button, a certificate link) are moved into the bar and shown only once everything is confirmed.

CSS classes

NameDescription
mv-trio-check-list / mv-trio-check-itemDefault list and row layout (number, text, control, status columns; the control goes under the prompt below 34rem of container width). Parts: -num, -text, -prompt, -hint, -control.
mv-trio-check-choices / mv-trio-check-choiceChoice chips: a group of labels wrapping native radios (arrow keys, focus ring, checked state).
mv-trio-check-status / mv-trio-check-sealConfirmed status of an item: the stamped seal (-seal-check path) and -status-text. The row gets data-trio-fresh for the stamp animation, staggered with --_i.
mv-trio-check-barSticky summary bar: -done-seal, -summary (-head with -count and -answered, -meter of -set groups of -tick[data-tick="empty | answered | confirmed"], -message[data-tone="miss | repeat | success | error | complete"]), -actions (-verify button with data-cooling, -extra for the complete slot).

CSS variables

NameDefaultDescription
--mv-trio-check-sealvar(--mv-success)Seal, confirmed rows and confirmed ticks.
--mv-trio-check-gapvar(--mv-space-2)Space between rows of the default list.
--mv-trio-check-sticky-offsetvar(--mv-space-3)Distance of the sticky bar from the bottom of the viewport while a long list scrolls.

Accessibility

The host is a labelled group; every rendered field has a real <label> (choice chips are a labelled radiogroup of native radios, arrow keys included) and hints are linked with aria-describedby. Nothing ever signals whether an individual answer is right, to sighted users or to screen readers alike. When a set is locked, text fields become readonly (still focusable and readable), selects and radios become disabled, and each control is described by the item’s status (“Confirmed and locked, set 2”); the visible “Confirmed” text and the check mark in the seal mean it never relies on color. If focus sat on a control that just became disabled (auto mode), it moves to the next open field, or to the Verify button. A polite live region announces each outcome once: “3 answers confirmed. 6 of 12 confirmed.”, “Not enough correct yet. Nothing was locked. You can try again in 5 seconds.”, “You can verify again.” when a cooldown ends, and “All 12 answers confirmed. Finished in 4 attempts.”; the visible message line repeats it and is linked to the Verify button with aria-describedby. The Verify button stays focusable when it cannot be used (aria-disabled, with the reason in the message: “Answer 2 more to verify.”), shows aria-busy while checking, and Ctrl/Cmd+Enter anywhere in the component verifies (aria-keyshortcuts). The meter is a role="progressbar" with “6 of 12 confirmed” as its value text. Reduced motion (OS or data-motion="reduce"): no stamp, ink ring, shake or draining fill, states change instantly and the countdown text remains. Forced colors: ticks and seals use system colors, confirmed rows get a solid outline. Locked selects and radios are disabled, so they are not submitted with a native form: read answers or confirmed instead.