익스클루시브

Read Back <mv-read-back>

조종사가 관제 지시를 복창하듯 중요한 폼을 위한 복창 확인 기능입니다. 폼을 감싸고 중요한 필드에 data-read-back을 표시하면, 제출할 때 인라인 패널이 각 값을 입력된 것과 다른 형태로 되돌려 보여 주므로 실수가 습관적으로 확인되지 않고 눈에 띄게 됩니다.

문화적 레퍼런스

변신 이야기(에코와 나르키소스 신화), 오비디우스 (8년, 책). 님프 에코는 자신에게 들려온 마지막 말을 되풀이하는 방식으로만 말할 수 있는 저주를 받았기에, 말하는 사람은 누구나 자기 말을 되돌려 듣게 됩니다. UI에서는 폼의 중요한 값을 확정하기 전에 일부러 다른 형태로(글자로 풀어 쓰기, 묶어 쓰기, 상대값, 단위와 자릿수 표기) 되읽어 주므로, 사용자는 자신이 실제로 입력한 내용을 듣고 실수를 잡아냅니다.

작동 방식

고위험 폼을 위한 복창 확인으로, 조종사가 관제 허가를 복창하는 방식과 같습니다: 폼을 감싸고 중요한 필드에 data-read-back을 표시하면, 제출 시 인라인 패널이 각 값을 입력된 형태와 다른 형태로 되읽어 주므로, 실수가 무심코 확인되는 대신 눈에 띄게 됩니다. 금액은 수표처럼 글자로 풀어 쓰고(“twelve thousand five hundred dollars and 00 cents”, 통화 인식, 자체 구현한 영어 숫자 단어) 애매한 구분 기호를 표시합니다(“1,25”는 $125.00로 읽힘). 계좌 번호는 묶어서 표시하고 검증하며(IBAN mod-97과 국가별 길이, 국가 이름 포함) 예금주 이름과 함께 보여 줍니다. 날짜는 요일과 상대 날짜로 바뀌고(“Friday, October 2 · in 8 days”, 연도는 올해가 아닐 때만 표시), 과거, 주말, 먼 미래이면 표시됩니다. 수량에는 단위, 글자 표기, 묶음 계산과 합계(“2 bottles × 150 = 300 mL · 2 × $8.40 = $16.80”)가 붙고, 소수점 안전 검사(앞에 0이 없는 “.5”, 뒤에 붙은 “5.0”)도 이루어집니다. 코드는 ICAO 음성 알파벳으로 풀어 쓰고 헷갈리는 문자(O/0, I/1)를 짚어 주며, 수정된 값은 이전 값과 함께 변경 부분을 강조해 보여 줍니다. 일반적인 값과 자릿수 단위로 차이 나면 문장으로 경고합니다(“10× your usual amount, an extra zero?”). 각 줄에는 정확히 해당 필드로 돌아가는 Change 버튼이 있고, 확인 시 마지막 4자, 무작위 문자 하나 또는 전체 값을 다시 입력하도록 요구할 수 있으며, 라이브 모드는 입력하는 동안 각 필드 아래에 같은 되읽기를 보여 줍니다. 사용자 정의 또는 비동기 포매터(은행 API로 수취인 이름 확인)를 필드나 유형별로 연결할 수 있고, 모든 것이 Intl을 인식하며(기본값 en-US), mv-read-back은 취소할 수 있고(소액이면 건너뛰기), mv-confirm도 취소할 수 있습니다(직접 fetch로 전송).

카테고리폼
유형Web Component (<mv-read-back>)
상태안정
키트사용자를 놓치지 않는 폼
함께 설치되는 항목button, input
Keywordsexclusive, culture, confirmation, read-back, verification, double-check, payment, wire-transfer, iban, amount-in-words, number-to-words, dosage, medication-safety, quantity, date, relative-date, phonetic-alphabet, diff, form, error-prevention, intl

When to use

  • A payment, wire transfer or payout must be checked before it leaves, where one extra zero or swapped digit is costly
  • Clinicians or pharmacists enter doses and quantities where a misplaced decimal point is dangerous
  • An admin edits production settings (DNS records, limits, bulk deletes) and should see exactly what changes before saving
  • Order quantities, pack sizes or dates are typed by hand and mistakes are only noticed after shipping or booking

Avoid when

  • The action is destructive but the values are not the risk; one deliberate gesture is the safeguard needed → use Hold Button instead
  • A critical action needs two people or two synchronized keys, not a value check → use Two-Key instead
  • The risk is something forgotten (attachment, subject, placeholder) rather than a value typed wrong → use Forgot instead

설치

node scripts/add.mjs read-back --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["read-back"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/input/input.css, core/dom.js, core/element.js, core/motion.js, components/read-back/read-back.js, components/read-back/read-back.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-read-back>
  <form>
    <label>Amount <input name="amount" value="12500" data-read-back="amount"></label>
    <label>Account number <input name="account" value="021000021" data-read-back="account"></label>
    <button>Send transfer</button>
  </form>
</mv-read-back>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="rb-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #rb-demo { display:grid; gap:1.25rem; align-content:start }
    #rb-demo .rb-card { min-width:0; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #rb-demo .rb-head { display:flex; align-items:center; gap:.75rem; padding:1rem 1.25rem; border-bottom:1px solid var(--mv-border) }
    #rb-demo .rb-head h3 { margin:0; font-size:.9375rem; font-weight:650; letter-spacing:-.01em }
    #rb-demo .rb-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #rb-demo .rb-glyph { display:grid; place-items:center; flex:none; width:2rem; height:2rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-muted); color:var(--mv-fg-muted) }
    #rb-demo .rb-glyph svg { width:1.0625rem; height:1.0625rem }
    #rb-demo mv-read-back { padding:1.125rem 1.25rem 1.25rem }
    #rb-demo form { display:grid; gap:1rem; margin:0 }
    #rb-demo .rb-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem 1.25rem; align-items:start }
    #rb-demo .rb-span { grid-column:1 / -1 }
    #rb-demo .rb-mono { font-family:var(--mv-font-mono); letter-spacing:.02em }
    #rb-demo .rb-amount { display:grid; grid-template-columns:minmax(0,1fr) 5.75rem; gap:.5rem }
    #rb-demo .rb-submit { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding-top:.25rem }
    #rb-demo .rb-submit p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; flex:1 1 16rem }
    #rb-demo .rb-controls { display:flex; align-items:center; gap:.75rem 1.5rem; flex-wrap:wrap; padding:.875rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #rb-demo .rb-controls .mv-choice { font-size:.8125rem }
    #rb-demo .rb-inline { display:flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.8125rem; font-weight:500 }
    #rb-demo .rb-inline .mv-select { width:auto; min-width:11.5rem }
    #rb-demo .rb-log { flex:1 1 14rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
    #rb-demo .rb-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.25rem; align-items:start }
    #rb-demo .rb-note { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    @media (max-width:46rem) {
      #rb-demo .rb-grid, #rb-demo .rb-pair { grid-template-columns:minmax(0,1fr) }
      #rb-demo .rb-log { text-align:start }
    }
  </style>

  <!-- Main: a wire transfer, live echo under each field + read-back on submit (already open here) -->
  <section class="rb-card" aria-labelledby="rb-wire-title">
    <header class="rb-head">
      <span class="rb-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10h18M5 10V20M9.5 10V20M14.5 10V20M19 10V20M3 20h18M12 3l9 5H3z"/></svg></span>
      <div>
        <h3 id="rb-wire-title">New international wire</h3>
        <p>From Operating account ···· 4417 · $48,210.55 available</p>
      </div>
    </header>
    <mv-read-back id="rb-wire" mode="both" challenge="last4">
      <form id="rb-wire-form" onsubmit="event.preventDefault()">
        <div class="rb-grid">
          <div class="mv-field">
            <label class="mv-label" for="rb-name">Beneficiary</label>
            <input class="mv-input" id="rb-name" name="beneficiary" value="Lindqvist &amp; Okafor GmbH" required autocomplete="off">
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-iban">IBAN</label>
            <input class="mv-input rb-mono" id="rb-iban" name="iban" value="DE89370400440532013000" data-read-back="account" data-holder-from="beneficiary" required spellcheck="false" autocomplete="off">
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-amount">Amount</label>
            <div class="rb-amount">
              <input class="mv-input" id="rb-amount" name="amount" inputmode="decimal" value="12500" data-read-back="amount" data-currency="currency" data-typical="1250" data-read-back-echo="#rb-amount-echo" required autocomplete="off">
              <select class="mv-select" id="rb-currency" name="currency" aria-label="Currency">
                <option selected>USD</option>
                <option>EUR</option>
                <option>GBP</option>
              </select>
            </div>
            <div id="rb-amount-echo"></div>
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-date">Execution date</label>
            <input class="mv-input" id="rb-date" name="date" type="date" data-read-back="date" data-business-days required>
          </div>
          <div class="mv-field rb-span">
            <label class="mv-label" for="rb-ref">Payment reference</label>
            <input class="mv-input rb-mono" id="rb-ref" name="reference" value="INV-2048-K7" data-read-back="code" required spellcheck="false" autocomplete="off">
          </div>
        </div>
        <div class="rb-submit">
          <p>Wires are read back before they leave: check every line, then confirm.</p>
          <button class="mv-button" type="submit">Send transfer</button>
        </div>
      </form>
    </mv-read-back>
    <div class="rb-controls">
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="rb-live" checked> Live echo under fields</label>
      <label class="rb-inline" for="rb-challenge">To confirm
        <select class="mv-select" data-size="sm" id="rb-challenge">
          <option value="">Nothing to re-type</option>
          <option value="last4" selected>Last 4 of the IBAN</option>
          <option value="char">One random character</option>
        </select>
      </label>
      <p class="rb-log" id="rb-log" aria-live="polite">Payee check: simulated, 500 ms.</p>
    </div>
  </section>

  <div class="rb-pair">
    <!-- Live only: a pharmacy order where decimal slips are dangerous -->
    <section class="rb-card" aria-labelledby="rb-rx-title">
      <header class="rb-head">
        <span class="rb-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="7" width="12" height="14" rx="2.5"/><path d="M8 3h8v4H8zM12 11v6M9 14h6"/></svg></span>
        <div>
          <h3 id="rb-rx-title">Pharmacy order</h3>
          <p>Ibuprofen oral suspension · 100 mg per 5 mL</p>
        </div>
      </header>
      <mv-read-back id="rb-rx" mode="live">
        <form onsubmit="event.preventDefault()">
          <div class="mv-field">
            <label class="mv-label" for="rb-dose">Dose per intake</label>
            <div class="mv-input-group">
              <input class="mv-input" id="rb-dose" inputmode="decimal" value="50" data-read-back="quantity" data-unit="mL" data-unit-name="milliliter" data-typical="5" data-typical-label="the usual dose" autocomplete="off">
              <span class="mv-input-group-addon">mL</span>
            </div>
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-supply">Supply</label>
            <input class="mv-input" id="rb-supply" type="number" min="1" value="2" data-read-back="quantity" data-unit="bottle" data-pack="150" data-pack-unit="mL" data-unit-price="8.40">
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-start">First intake</label>
            <input class="mv-input" id="rb-start" type="date" data-read-back="date">
          </div>
        </form>
      </mv-read-back>
    </section>

    <!-- Edit form: only what changed is read back, with the change highlighted -->
    <section class="rb-card" aria-labelledby="rb-dns-title">
      <header class="rb-head">
        <span class="rb-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.8 3 2.8 15 0 18M12 3c-2.8 3-2.8 15 0 18"/></svg></span>
        <div>
          <h3 id="rb-dns-title">Edit DNS record</h3>
          <p>api.example.com · production zone</p>
        </div>
      </header>
      <mv-read-back id="rb-dns" mode="both" only-changed>
        <form onsubmit="event.preventDefault()">
          <div class="rb-grid">
            <div class="mv-field">
              <label class="mv-label" for="rb-type">Type</label>
              <select class="mv-select" id="rb-type" data-read-back="text">
                <option selected>A</option>
                <option>AAAA</option>
                <option>CNAME</option>
              </select>
            </div>
            <div class="mv-field">
              <label class="mv-label" for="rb-ttl">TTL (seconds)</label>
              <input class="mv-input" id="rb-ttl" type="number" min="60" value="300" data-was="3600" data-read-back="quantity" data-unit="second">
            </div>
          </div>
          <div class="mv-field">
            <label class="mv-label" for="rb-target">Points to</label>
            <input class="mv-input rb-mono" id="rb-target" value="203.0.113.101" data-was="203.0.113.10" data-read-back="text" spellcheck="false" autocomplete="off">
          </div>
          <div class="rb-submit">
            <p class="rb-note" id="rb-dns-note" aria-live="polite">Unsaved changes.</p>
            <button class="mv-button" type="submit">Save record</button>
          </div>
        </form>
      </mv-read-back>
    </section>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-read-back");
    const wire = document.getElementById("rb-wire");
    const rx = document.getElementById("rb-rx");
    const dns = document.getElementById("rb-dns");
    const log = document.getElementById("rb-log");
    const say = (text) => { log.textContent = text; };
    const pad = (n) => String(n).padStart(2, "0");
    const iso = (days) => { const d = new Date(); d.setDate(d.getDate() + days); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
    for (const [id, days] of [["rb-date", 8], ["rb-start", 1]]) {
      const el = document.getElementById(id);
      el.defaultValue = iso(days);
      el.value = iso(days);
    }

    // Simulated confirmation of payee: an async formatter adds a line once the bank answers.
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    wire.formatters = {
      iban: async ({ echo, field }) => {
        await sleep(500);
        const name = field.form.elements.beneficiary.value.trim();
        const known = /lindqvist/i.test(name);
        return known
          ? { detail: [echo.detail, "Payee name matches the bank’s records"].filter(Boolean) }
          : { detail: echo.detail, warning: "Payee name doesn’t match the bank’s records" };
      },
    };
    rx.refresh();
    wire.open({ focus: false });

    wire.addEventListener("mv-read-back", (e) => say(`Read-back: ${e.detail.items.length} values to check.`));
    wire.addEventListener("mv-back", (e) => say(e.detail.item ? `Back to “${e.detail.item.label}”.` : "Back to the form."));
    wire.addEventListener("mv-confirm", (e) => {
      e.preventDefault(); // the demo sends nothing
      const amount = e.detail.items.find((i) => i.type === "amount");
      say(`Confirmed: ${amount?.text ?? "wire"} released.`);
    });
    document.getElementById("rb-live").addEventListener("change", (e) => { wire.mode = e.target.checked ? "both" : "submit"; });
    document.getElementById("rb-challenge").addEventListener("change", (e) => { wire.challenge = e.target.value || null; });

    const note = document.getElementById("rb-dns-note");
    dns.addEventListener("mv-confirm", (e) => { e.preventDefault(); note.textContent = "Record saved. Propagation can take up to 5 minutes."; });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
modesubmit | live | bothsubmitsubmit: the read-back panel opens when the form is submitted. live: the echo is shown under each field while typing, submits are not held. both: live echo plus the read-back on submit.
challengelast4 | char | fullWhat must be re-typed before Confirm works: the last 4 characters, one randomly chosen character (its position is named, e.g. “the 7th character”) or the whole value. Applies to the field marked data-read-back-challenge, else the first account, amount or code field. Amounts and quantities always use full (compared as numbers); values shorter than 6 characters fall back to full.
forstring (id)Id of the form to guard when <mv-read-back> does not wrap it (default: a child <form>, else the closest ancestor form).
localeBCP 47 tagen-USFormatting locale for numbers, currencies, dates, relative days and country names; also used to parse typed numbers (group and decimal separators). Spelled-out words are English; other locales fall back to the Intl long form (“12.500,00 Euro”) unless you provide a formatter.
currencyISO 4217 codeUSDDefault currency of amount fields (per field: data-currency).
rationumber3How far from data-typical a value must be (above or below, as a factor) before it is flagged. Near powers of ten add “an extra zero?” / “a missing zero?”.
changesbooleanEdit forms: values that differ from their initial value (defaultValue, defaultSelected, defaultChecked or data-was) show “was …”, and text or code changes are highlighted.
only-changedbooleanImplies changes; only fields whose value moved are read back (fields with no initial value, or with data-read-back-always, still are).
data-read-backamount | account | date | quantity | code | text | <custom>On a field (input, select, textarea, radio, or a fieldset / radiogroup of choices): echo it, formatted by type. Empty: inferred (date inputs → date, number inputs → quantity, else text). Any other name is echoed as text and looked up in formatters.
data-read-back-labelstringName shown and announced for the field (default: its <label>, legend, aria-label, aria-labelledby, placeholder, then name).
data-currencyISO code | field name or idAmount fields: currency code, or the name/id of the field holding it (a currency select).
data-typicalnumberAmount and quantity fields: the usual value; values ratio× above or below it get a magnitude warning in words.
data-typical-labelstringyour usual amount | your usual quantityHow the typical value is named in the warning (“10× the usual dose”).
data-unit / data-unit-plural / data-unit-namestringQuantity fields: unit shown after the number (word units such as “box” are pluralized, symbols such as “mL” are not), an irregular plural, and the unit’s spoken name used in words (“milliliter”).
data-pack / data-pack-unitnumber / stringQuantity fields: items per unit, for the total line (“12 boxes × 24 = 288 units”).
data-unit-pricenumberQuantity fields: price per unit, for a total (“2 × $8.40 = $16.80”) in the field’s currency.
data-holder / data-holder-fromstring / field name or idAccount fields: the holder’s name, literally or read from another field, shown next to the grouped number.
data-groupsizes, e.g. "3,3,4"4Account fields: grouping pattern (the last size repeats).
data-business-daysbooleanDate fields: flag Saturdays and Sundays.
data-allow-pastbooleanDate fields: don’t flag past dates.
data-case-sensitivebooleanCode fields: case matters (mixed-case letters are spelled “lowercase Kilo”, the challenge compares case).
data-read-back-echoCSS selectorLive mode: container that receives the field’s echo line (default: right after the field or its .mv-input-group).
data-read-back-challengebooleanMarks the field whose value must be re-typed when challenge is set.
data-wasstringPrevious value of the field (overrides the initial value) and turns change tracking on for it.
data-read-back-skipbooleanOn a submit button (e.g. “Save draft”): submits without a read-back.
data-read-back-panelbooleanOn an element inside the form or the component: the read-back panel is placed there instead of at the end of <mv-read-back>.
data-open / data-pendingset by the componentOn the host: while the panel is shown / while an async formatter is still answering (Confirm is aria-disabled).

Properties

Name유형Description
formattersRecord<string, (ctx) => string | EchoPatch | Promise<EchoPatch>>Custom echo, keyed by field name, id or data-read-back type (in that order). ctx: { field, type, raw, value (parsed number / Date / string), locale, currency, echo: { value, words, detail, warnings } }. Return a string (replaces the main value) or { value?, words?, detail?, warnings?, warning? } merged over the default echo. A promise shows “Checking…” and holds Confirm until it settles; a rejection adds “Couldn’t verify this value”.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (title, text, flags, change, back, confirm ({action}), confirmPlain, checking, was, holder, challengeLast4, challengeChar, challengeFull, challengeMismatch, above, below, extraZero, missingZero, separator, leadingZero, trailingZero, past, weekend, farFuture, ibanValid, ibanInvalid, ibanLength, digits, lookAlike, whitespace, lookupFailed, announce*…). English defaults.
nowDate | numberReference “today” for relative dates and past/future flags (default: the current time).
itemsReadBackItem[]Read-only: the echo shown in the open panel ({ field, name, type, label, raw, value, text, words, detail, warnings, was, changed, pending, spoken }); empty when closed.
isOpenbooleanRead-only: the panel is shown.
mode / challenge / locale / currency / ratio / changes / onlyChangedreflectedMirror the attributes.

Methods

NameDescription
open({ submitter?, focus? })Shows the read-back for the current values without a submit (no mv-read-back event); focus defaults to true. Returns the items.
close({ focus? })Hides the panel without submitting.
confirm()Same as the Confirm button: checks the challenge, emits mv-confirm and submits. Returns true when confirmed.
echo(field | selector)Promise of one field’s echo (formatters included), for your own UI or logs.
refresh()Recomputes the live echoes and the open panel after values were changed from code.

Events

NameDescription
mv-read-backCancelable, before the panel opens on submit. detail: { items, form, submitter }. preventDefault() skips the read-back for this submit and lets the form go through (e.g. small amounts).
mv-confirmCancelable: the user confirmed the read-back (challenge passed). detail: { items, form, submitter }. Not cancelled: the form is submitted with the original submitter (requestSubmit). preventDefault(): nothing is submitted, send it yourself.
mv-backThe user went back instead of confirming. detail: { field (the field of the Change button, null for Go back / Escape), item }.

Content structure

NameDescription
(content)A <form> (or any content when for= points to a form elsewhere). The panel and a visually hidden live region are appended to the component; live echo lines are inserted after each field.

CSS classes

NameDescription
mv-read-back-panelThe read-back (role=group): -head (-icon, -title, -text, -flag), -list of .mv-read-back-item, -challenge (-challenge-label, -challenge-input, -error), -actions (-back, -confirm). data-warn when a line is flagged.
mv-read-back-itemOne echoed value: -label, -echo (-value, -words, -detail, -warning), -edit. data-type, data-warn, data-changed, data-pending.
mv-read-back-diff<mark> around the changed part of an edited text or code value.
mv-read-back-liveLive echo line under a field: -live-main (-live-icon, -live-value, -live-part), -live-warning. data-type, data-warn.

CSS variables

NameDefaultDescription
--mv-read-back-warnvar(--mv-warning)Color of flagged lines, warnings and the count badge (always paired with an icon and words).
--mv-read-back-accentvar(--mv-accent)Tint of the panel icon and the diff underline.
--mv-read-back-diffaccent at 22%Background of the highlighted change.

Accessibility

The read-back is plain text, never an image or a color: when it opens on submit, focus moves to its title (a focusable heading-like paragraph labelling a role="group" panel, described by its instructions) and a polite live region reads the whole echo once (“Read back, 4 values. Amount: $12,500.00. twelve thousand five hundred dollars and 00 cents. Check: 10× your usual amount, an extra zero?…”); later edits while it is open are announced as “Read back updated”, async checks as they resolve. Account numbers are exposed to screen readers character by character (“D E 8 9, 3 7 0 4…”) instead of as big numbers, codes through their phonetic spelling. Every line has a real <button> “Change” whose accessible name includes the field (“Change Amount”): it closes the panel, focuses the exact field (the checked radio for a group), scrolls it into view and selects its text. Go back and Escape return to the first flagged field, else to the submit button; Confirm is a real button labelled with the original action (“Correct, send transfer”), aria-disabled and aria-busy while a check is pending (pressing it announces why). The challenge field has a real <label> naming what to type, aria-invalid and an error linked with aria-describedby; Enter confirms. After confirming, focus returns to the submit button. Warnings are never color-only: a warning icon, a visually hidden “Check:” prefix, words, a left bar and a count badge (“1 to double-check”); the changed part of an edited value is both tinted and underlined. Live echo lines are linked to their field with aria-describedby (read when the field is focused) and are not live regions, so typing never chatters. Reduced motion (OS or data-motion="reduce"): no entrance slide and no pending pulse. Forced colors: borders and warnings use CanvasText, diffs use Highlight.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고