Exclusivobeta
Trio Check <mv-trio-check>
Verificação em lote para quizzes, testes de conhecimento e exercícios de associação que torna inútil o chute: as respostas são inseridas livremente (seus próprios inputs, selects, rádios ou checkboxes marcados com data-trio-item, ou linhas renderizadas a partir de uma propriedade items com campos de texto, selects e chips de escolha), e o componente nunca revela se uma resposta isolada está certa. Só quando um conjunto inteiro de respostas (3 por padrão, batch) está certo de uma vez ele é confirmado: as linhas passam a ser somente leitura e ganham um selo carimbado com “Confirmed”, o medidor preenche um conjunto, e o resto continua ambíguo (4 respostas certas confirmam 3; o último conjunto se reduz ao que sobra). Uma tentativa fracassada só diz “Not enough correct yet. Nothing was locked.” com um pequeno tremor do medidor, um reenvio idêntico é recusado sem gastar uma tentativa, e um cooldown opcional esvazia o botão Verify antes da próxima tentativa. A correção vem de um callback assíncrono check(answers) (o seu servidor, que pode aplicar a mesma regra e devolver só os ids que confirma) ou de hashes SHA-256 com sal em data-answer-hash (com um utilitário estático hash() e normalização de maiúsculas, espaços e acentos), para que as respostas nunca cheguem ao navegador em texto claro. Uma barra fixa mostra “6 of 12 confirmed · 2 answered”, a verificação pode rodar automaticamente a cada mudança, o progresso pode ser restaurado a partir do servidor, e um estado de conclusão (selo grande, número de tentativas, seu próprio botão de continuar) encerra o exercício.
| Categoria | Formulários |
|---|---|
| Tipo | Web Component (<mv-trio-check>) |
| Status | beta |
| Também instala | button, input, select |
| Keywords | exclusive, 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
- A compliance or onboarding knowledge check must show real understanding, not answers found by trial and error one field at a time
- A matching exercise pairs terms with definitions, tickets with actions or people with roles, and each pair should not be checkable alone
- A language or vocabulary drill checks typed answers in the browser without shipping them in clear
- A puzzle hunt or escape game confirms deductions in sets so players have to reason rather than brute-force
Avoid when
- Each answer should get immediate right or wrong feedback, as in a practice drill or a form validation → use Field instead
- The questions are a guided sequence where each step depends on the previous answer → use Stepper instead
- A graded, high-stakes exam: client-side hashes of short or multiple-choice answers can be brute-forced, so check on the server only
Instalação
node scripts/add.mjs trio-check --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["trio-check"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): 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.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<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>Referência cultural
Return of the Obra Dinn, Lucas Pope (2018, jogo). O livro de registros do seguro só confirma suas deduções em lotes: os destinos só são confirmados como corretos quando três estão certos ao mesmo tempo, para que não possam ser descobertos por força bruta um a um. Na interface, as respostas só são confirmadas em conjuntos de três (ou de qualquer tamanho de lote), seladas juntas, enquanto uma tentativa fracassada não revela nada sobre qual resposta estava errada.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
batch | number | 3 | How 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. |
cooldown | time ("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. | |
auto | boolean | Verify 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. | |
salt | string | Salt mixed into data-answer-hash hashes (see hash()). Ignored when check is set. | |
normalize | fold | trim | exact | fold | How 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. |
label | string | Knowledge check | Accessible name of the group (aria-label on the host, unless it has aria-labelledby). |
data-trio-item | string (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-hash | hex SHA-256, space or comma separated | On an item: accepted answer hashes (several for synonyms such as “cocina” and “la cocina”). Used when no check callback is set. | |
data-trio-field | marker attribute | Inside 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-value | string | On an item: the answer itself, set by your code for custom widgets (drag and drop, maps, pickers). Takes precedence over any field. | |
data-trio-status | marker attribute | Inside an item: where the confirmed status (seal + “Confirmed”) is written. Otherwise a .mv-trio-check-status span is appended to the item. | |
data-trio-state | empty | answered | confirmed | Set 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-state | idle | ready | pending | cooldown | complete | Set on the host: not enough answers yet, verifiable, checking, cooling down, all confirmed. aria-busy is set while checking. |
Properties
| Name | Tipo | Description |
|---|---|---|
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. |
items | Array<{ 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. |
strings | Partial<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. |
answers | Record<id, string> | Current value of every item, confirmed or not (read-only). |
confirmed | string[] | 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 / cooling | number / boolean / number (ms) | Attempts spent, whether a check is in flight, time left in the cooldown (read-only). |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-attempt | Cancelable, 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-confirm | Items were locked. detail: { ids, items (elements), attempt, source (the attempt reason, or "api"), confirmed, total }. |
mv-miss | An attempt locked nothing. detail: { attempt, answered, cooldown (ms, 0 when none) }. Never says which answers were wrong. |
mv-complete | Every item is confirmed. detail: { attempts, total, duration (ms since the first attempt) }. |
mv-check-error | The check callback rejected, or hashing failed (insecure context). detail: { attempt, error }. |
Content structure
| Name | Description |
|---|---|
(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. |
complete | Children with slot="complete" (a Continue button, a certificate link) are moved into the bar and shown only once everything is confirmed. |
CSS classes
| Name | Description |
|---|---|
mv-trio-check-list / mv-trio-check-item | Default 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-choice | Choice chips: a group of labels wrapping native radios (arrow keys, focus ring, checked state). |
mv-trio-check-status / mv-trio-check-seal | Confirmed 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-bar | Sticky 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
| Name | Default | Description |
|---|---|---|
--mv-trio-check-seal | var(--mv-success) | Seal, confirmed rows and confirmed ticks. |
--mv-trio-check-gap | var(--mv-space-2) | Space between rows of the default list. |
--mv-trio-check-sticky-offset | var(--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.