独家

Prep List <mv-prep-list>

适用于漫长或高风险流程的“开始之前”准备卡片(贷款、签证或保险申请、身份验证、报税、员工入职、数据库迁移、生产发布)。

文化出处

Kitchen Confidential,安东尼·波登(2000 年,书籍)。灶台厨师会做好 mise en place(就位准备):每样食材和工具都在开餐前备好,放在工位上触手可及之处,因为在高峰期中途才发现缺了什么就意味着灾难。在界面中,长流程或高风险流程以一张准备卡片开头,列出用户需要什么、为什么需要、在哪里找到,让用户勾选手头已有的,或将某些项目留待稍后,并在一个能感知准备状态的 Start 按钮之前给出诚实的时间估计。

工作原理

“Before you start” 准备卡片,用于长流程或高风险流程(贷款、签证或保险申请、身份验证、报税、员工入职、数据库迁移、生产发布)。它列出用户需要的一切(文件、号码、权限、设备、决定、其他人),说明每项为什么需要、在哪里找到(“In your bank app, under Account details”,保留链接)以及哪个步骤会用到它,并提供深层链接。用户勾选已经具备的项目,或将某项标记为 “I’ll get it later”:这不算错误,流程只是会在那一步暂停。诚实的时间估计会把流程本身的时长加上收集尚未就绪项目所需的时间(“About 35 min · 15 min with everything at hand, plus 20 min to gather 2 items”),分段进度条(实心表示就绪,斜线表示稍后)让准备情况一目了然。项目就绪后其详情会收起,工位随着准备而逐渐清爽。Start 按钮始终说明自己的状态(“2 items not ready yet. You can still begin…”),只有在缺少开始前必需的项目时才会阻止,此时按下它会跳到该项目,而不是静默失败。开始后,卡片会收起为一个紧凑的 “Prepared · 5 of 7 ready · 1 for later” chip,点击可重新打开列表,以便在流程中途勾选项目。流程可读取状态(ready、canStart、missing、later、getState())并监听 mv-prep-change;mv-start 可取消,进度可通过选择启用的存储键按浏览器持久保存,并在标签页之间同步。

分类表单
类型Web Component(<mv-prep-list>)
状态稳定版
套件不让用户流失的表单
同时安装button, checkbox
Keywordsexclusive, culture, checklist, prerequisites, before-you-start, preparation, readiness, onboarding, application, kyc, verification, tax, runbook, release, time-estimate, defer, resume, persistence, local-storage, form

When to use

  • A long application or verification flow fails midway when users lack a document, number or login they could have gathered first
  • Users should see an honest time estimate and what to prepare before committing to a tax filing, visa or insurance form
  • An onboarding, migration or release runbook needs its prerequisites confirmed, with only a few truly blocking the start
  • The flow can pause at a step and resume later, so a missing item should be deferred instead of blocking everything

Avoid when

  • The user is already inside the flow and needs to see or move between its steps → use Stepper instead
  • The goal is to nudge a user who is stuck on a form, not to prepare before starting → use Insist instead
  • The flow takes under a minute and needs nothing the user doesn’t already know: a checklist only adds friction

安装

node scripts/add.mjs prep-list --out ./src/marvelous

通过 Marvelous UI MCP 服务器使用 AI 智能体:install_components({ slugs: ["prep-list"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, core/dom.js, core/element.js, core/motion.js, components/prep-list/prep-list.js, components/prep-list/prep-list.css。

用法

快速开始,最小可运行标记:

<mv-prep-list heading="Before you apply" duration="15m" start-label="Start application">
  <div data-prep="photo-id" data-prep-kind="document" data-prep-requirement="required-to-start"><strong>Photo ID</strong><p data-prep-why>We match it with a selfie.</p></div>
  <div data-prep="iban" data-prep-kind="number" data-prep-time="3m"><strong>Your bank account number</strong><p data-prep-where>In your bank app, under Account details.</p></div>
</mv-prep-list>

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<div id="pl-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #pl-demo { display:grid; gap:2.5rem }
    #pl-demo .pl-layout { display:grid; grid-template-columns:minmax(0,1fr) 19rem; gap:1.25rem; align-items:start }
    #pl-demo .pl-flow { position:sticky; top:1rem; display:grid; gap:1rem; padding:1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface-raised); box-shadow:var(--mv-shadow-sm); font-size:.8125rem }
    #pl-demo .pl-flow-head { display:flex; align-items:center; justify-content:space-between; gap:.5rem }
    #pl-demo .pl-flow-head h4 { margin:0; font-size:.875rem; font-weight:600; letter-spacing:-.01em }
    #pl-demo .pl-k { margin:0; color:var(--mv-fg-muted); font-size:.6875rem; letter-spacing:.04em; text-transform:uppercase; font-weight:600 }
    #pl-demo .pl-steps { display:grid; gap:.25rem; margin:0; padding:0; list-style:none; counter-reset:pl }
    #pl-demo .pl-step { display:grid; grid-template-columns:1.5rem minmax(0,1fr) auto; gap:.125rem .625rem; align-items:center; padding:.5rem .5rem; border-radius:var(--mv-radius-md); transition:background-color var(--mv-duration-normal) var(--mv-ease-out), box-shadow var(--mv-duration-normal) var(--mv-ease-out) }
    #pl-demo .pl-step::before { counter-increment:pl; content:counter(pl); display:grid; place-items:center; width:1.5rem; height:1.5rem; border-radius:50%; background:var(--mv-bg-muted); color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; font-variant-numeric:tabular-nums }
    #pl-demo .pl-step[data-current]::before { background:var(--mv-accent); color:var(--mv-fg-on-accent) }
    #pl-demo .pl-step[data-flash] { background:var(--mv-accent-subtle); box-shadow:inset 0 0 0 1px color-mix(in oklab, var(--mv-accent) 40%, transparent) }
    #pl-demo .pl-step b { font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #pl-demo .pl-step small { grid-column:2 / -1; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.35 }
    #pl-demo .pl-step small:empty { display:none }
    #pl-demo .pl-find { padding:0; border:0; background:none; color:var(--mv-accent); font:inherit; font-weight:500; text-decoration:underline; text-underline-offset:2px; cursor:pointer; border-radius:var(--mv-radius-xs) }
    #pl-demo .pl-find:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #pl-demo .pl-sep { height:1px; margin:0; border:0; background:var(--mv-border) }
    #pl-demo .pl-state { display:grid; grid-template-columns:auto 1fr; gap:.25rem .75rem; margin:0; font-size:.75rem }
    #pl-demo .pl-state dt { color:var(--mv-fg-muted) }
    #pl-demo .pl-state dd { margin:0; font-family:var(--mv-font-mono); font-size:.6875rem; text-align:end; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #pl-demo .pl-log { min-height:2.5rem; margin:0; padding:.5rem .625rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); color:var(--mv-fg-muted); font:.6875rem/1.5 var(--mv-font-mono); overflow-wrap:anywhere }
    #pl-demo .pl-controls { display:grid; gap:.625rem }
    #pl-demo .pl-controls .mv-choice { font-size:.8125rem }
    #pl-demo .pl-row { display:flex; gap:.5rem; flex-wrap:wrap }
    #pl-demo .pl-section-head { display:grid; gap:.25rem; margin:0 0 1rem }
    #pl-demo .pl-section-head h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
    #pl-demo .pl-section-head p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:44rem }
    #pl-demo .pl-release { max-width:46rem }
    @media (max-width:56rem) {
      #pl-demo .pl-layout { grid-template-columns:minmax(0,1fr) }
      #pl-demo .pl-flow { position:static }
    }
  </style>

  <!-- Main: a personal loan application. Two items are ready to find, one is set aside for later. -->
  <div class="pl-layout">
    <mv-prep-list id="pl-loan" heading="Before you apply for your personal loan" heading-level="3" duration="15m" start-label="Start application">
      <p slot="intro">The application itself takes about 15 minutes when you have everything below. Nothing is submitted until you sign, and you can stop at any step.</p>

      <div data-prep="amount" data-prep-kind="decision" data-prep-time="5m" data-prep-step="Step 1 · Loan amount" data-prep-href="#pl-step-1" data-prep-state="ready">
        <strong>How much to borrow, and over how long</strong>
        <p data-prep-why>Your rate depends on both. You can still adjust them until you sign.</p>
        <p data-prep-where>Use the loan calculator on the previous page. Most people choose 36 or 60 months.</p>
      </div>
      <div data-prep="phone" data-prep-kind="device" data-prep-requirement="required-to-start" data-prep-step="Step 2 · Identity" data-prep-href="#pl-step-2" data-prep-state="ready">
        <strong>Your mobile phone</strong>
        <p data-prep-why>We text a 6-digit code to confirm it’s you, and again when you e-sign.</p>
        <p data-prep-where>Use the number your bank has on file, so the check goes through instantly.</p>
      </div>
      <div data-prep="photo-id" data-prep-kind="document" data-prep-time="2m" data-prep-requirement="required-to-start" data-prep-step="Step 2 · Identity" data-prep-href="#pl-step-2" data-prep-state="ready">
        <strong>A photo ID</strong>
        <p data-prep-why>We match it against a quick selfie, so no one can apply in your name.</p>
        <p data-prep-where>Driver’s license, state ID or passport: the physical card, not a photocopy or a picture of it.</p>
      </div>
      <div data-prep="ssn" data-prep-kind="number" data-prep-time="5m" data-prep-step="Step 3 · About you" data-prep-href="#pl-step-3">
        <strong>Your Social Security number</strong>
        <p data-prep-why>It’s used for a soft credit check that won’t affect your score.</p>
        <p data-prep-where>On your Social Security card, or on last year’s tax return (<b>Form 1040</b>, page 1).</p>
      </div>
      <div data-prep="paystubs" data-prep-kind="document" data-prep-time="10m" data-prep-step="Step 4 · Income" data-prep-href="#pl-step-4">
        <strong>Your two most recent pay stubs</strong>
        <p data-prep-why>They confirm your income, which sets how much you can borrow.</p>
        <p data-prep-where>Your employer’s payroll portal, usually under <b>Pay › Statements</b>. PDFs or clear photos both work.</p>
      </div>
      <div data-prep="bank" data-prep-kind="number" data-prep-time="3m" data-prep-step="Step 5 · Payout" data-prep-href="#pl-step-5" data-prep-state="later">
        <strong>Bank account and routing numbers</strong>
        <p data-prep-why>The loan is paid into this account, and repayments come out of it.</p>
        <p data-prep-where>In your bank app under <b>Account details</b>, or printed at the bottom of a check.</p>
      </div>
      <div data-prep="co-applicant" data-prep-kind="person" data-prep-requirement="optional" data-prep-step="Step 3 · About you" data-prep-href="#pl-step-3">
        <strong>Your co-applicant’s details</strong>
        <p data-prep-why>Only if you apply jointly: their name, date of birth and income.</p>
        <p data-prep-where>Ask them beforehand; they’ll receive a text to confirm.</p>
      </div>
    </mv-prep-list>

    <aside class="pl-flow" aria-label="Application flow (demo)">
      <div class="pl-flow-head">
        <h4>Your application</h4>
        <span class="mv-badge" data-variant="secondary" id="pl-flow-badge">Not started</span>
      </div>
      <ol class="pl-steps" id="pl-steps">
        <li class="pl-step" id="pl-step-1" data-items="amount"><b>Loan amount</b><span></span><small></small></li>
        <li class="pl-step" id="pl-step-2" data-items="phone photo-id"><b>Identity</b><span></span><small></small></li>
        <li class="pl-step" id="pl-step-3" data-items="ssn co-applicant"><b>About you</b><span></span><small></small></li>
        <li class="pl-step" id="pl-step-4" data-items="paystubs"><b>Income</b><span></span><small></small></li>
        <li class="pl-step" id="pl-step-5" data-items="bank"><b>Payout account</b><span></span><small></small></li>
        <li class="pl-step" id="pl-step-6" data-items=""><b>Review &amp; e-sign</b><span></span><small></small></li>
      </ol>
      <hr class="pl-sep">
      <p class="pl-k">What the flow reads</p>
      <dl class="pl-state">
        <dt>ready</dt><dd id="pl-ready">…</dd>
        <dt>canStart</dt><dd id="pl-can">…</dd>
        <dt>later</dt><dd id="pl-later">…</dd>
      </dl>
      <p class="pl-log" id="pl-log" aria-live="polite">Tick an item, set one aside, or press Start.</p>
      <div class="pl-controls">
        <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="pl-remember"> Remember progress on this device</label>
        <div class="pl-row">
          <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="pl-upload">Pay stubs uploaded in the flow</button>
          <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="pl-reset">Reset</button>
        </div>
      </div>
    </aside>
  </div>

  <!-- Variant: a release runbook. One required-to-start item is missing, so Start explains and points to it. -->
  <section>
    <div class="pl-section-head">
      <h3>Blocked only by what truly blocks</h3>
      <p>Only the deploy permission is required to start. Press Start anyway: it points you to what’s missing instead of failing silently. The rollback plan is set aside for later; the release will pause at step 5.</p>
    </div>
    <mv-prep-list id="pl-release" class="pl-release" heading="Before you deploy v4.12" duration="40m" start-label="Start release">
      <p slot="intro">Thursday release window, 2:00-3:00 PM. Four services, one schema migration.</p>
      <a slot="footer" class="mv-button" data-variant="ghost" href="#pl-release">View runbook</a>

      <div data-prep="deploy-access" data-prep-kind="access" data-prep-time="10m" data-prep-requirement="required-to-start" data-prep-step="Step 1 · Deploy">
        <strong>Production deploy permission</strong>
        <p data-prep-why>The pipeline asks for approval from someone holding deploy rights before anything ships.</p>
        <p data-prep-where>Access portal › <b>Engineering</b> › <b>Production deploy</b>. Approval usually takes a few minutes.</p>
      </div>
      <div data-prep="snapshot" data-prep-kind="document" data-prep-time="15m" data-prep-step="Step 3 · Migrate" data-prep-state="ready">
        <strong>Today’s database snapshot</strong>
        <p data-prep-why>Step 3 runs a schema migration; the snapshot is your way back.</p>
        <p data-prep-where>Cloud console › Databases › orders-prod › Snapshots. Take a manual one if the latest is over 24 hours old.</p>
      </div>
      <div data-prep="rollback" data-prep-kind="decision" data-prep-time="5m" data-prep-step="Step 5 · Verify" data-prep-state="later">
        <strong>Rollback owner and trigger</strong>
        <p data-prep-why>Decide who rolls back, and at what error rate, before anyone is tired.</p>
        <p data-prep-where>Write both in the release ticket. Team default: 2% of requests failing for 5 minutes.</p>
      </div>
      <div data-prep="on-call" data-prep-kind="person" data-prep-time="2m" data-prep-requirement="optional">
        <strong>On-call engineer in the loop</strong>
        <p data-prep-why>They won’t be surprised by alerts during the release window.</p>
        <p data-prep-where>Post the window in the team’s incidents channel and tag whoever is on call.</p>
      </div>
    </mv-prep-list>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-prep-list");
    const prep = document.getElementById("pl-loan");
    const release = document.getElementById("pl-release");
    const steps = [...document.querySelectorAll("#pl-steps .pl-step")];
    const badge = document.getElementById("pl-flow-badge");
    const log = document.getElementById("pl-log");
    const remember = document.getElementById("pl-remember");
    const out = (id, v) => { document.getElementById(id).textContent = v; };
    const say = (text) => { log.textContent = text; };
    const labelOf = (id) => prep.items.find((it) => it.id === id)?.label ?? id;

    // The flow reads the checklist: which steps will pause, which are covered.
    function render() {
      out("pl-ready", String(prep.ready));
      out("pl-can", String(prep.canStart));
      out("pl-later", prep.later.length ? JSON.stringify(prep.later) : "[]");
      const started = prep.started;
      badge.textContent = started ? "In progress" : "Not started";
      badge.dataset.variant = started ? "info" : "secondary";
      steps.forEach((li, i) => {
        const ids = li.dataset.items.split(" ").filter(Boolean);
        const needed = ids.filter((id) => prep.items.find((it) => it.id === id)?.requirement !== "optional");
        const later = needed.filter((id) => prep.getState(id) === "later");
        const missing = needed.filter((id) => prep.getState(id) === "missing");
        const tag = li.querySelector("span");
        const note = li.querySelector("small");
        tag.replaceChildren();
        note.replaceChildren();
        li.toggleAttribute("data-current", started && i === 0);
        if (!needed.length) return;
        const b = document.createElement("span");
        b.className = "mv-badge";
        b.dataset.size = "sm";
        if (later.length) { b.dataset.variant = "warning"; b.textContent = "Pauses here"; }
        else if (missing.length) { b.dataset.variant = "secondary"; b.textContent = `${missing.length} missing`; }
        else { b.dataset.variant = "success"; b.textContent = "Covered"; }
        tag.append(b);
        const first = later[0] ?? missing[0];
        if (first) {
          const find = document.createElement("button");
          find.type = "button";
          find.className = "pl-find";
          find.textContent = "Show in checklist";
          find.addEventListener("click", () => prep.focusItem(first));
          note.append(`Needs ${labelOf(first).replace(/^Your /, "your ")}. `, find);
        }
      });
    }

    prep.addEventListener("mv-prep-change", (e) => {
      const d = e.detail;
      say(d.id ? `mv-prep-change · ${d.id}: ${d.previous} → ${d.state} (${d.source})` : `mv-prep-change · synced from another tab`);
      render();
    });
    prep.addEventListener("mv-start", (e) => {
      const d = e.detail;
      say(`mv-start · ${d.counts.ready} of ${d.counts.total} ready, pauses at: ${d.later.join(", ") || "none"}`);
      requestAnimationFrame(render);
    });
    // Deep links stay in the page here: highlight the step instead of jumping.
    prep.addEventListener("mv-prep-goto", (e) => {
      e.preventDefault();
      const li = document.querySelector(e.detail.href);
      if (!li) return;
      li.setAttribute("data-flash", "");
      setTimeout(() => li.removeAttribute("data-flash"), 1200);
      say(`mv-prep-goto · ${e.detail.id} → ${e.detail.step}`);
    });

    remember.addEventListener("change", () => {
      prep.storageKey = remember.checked ? "pl-demo-loan" : null;
      say(remember.checked ? "Progress is now kept in this browser (storage-key)." : "Progress is no longer stored.");
    });
    document.getElementById("pl-upload").addEventListener("click", () => prep.setItem("paystubs", "ready"));
    document.getElementById("pl-reset").addEventListener("click", () => {
      prep.reset();
      render();
      say("Reset to the initial checklist.");
    });

    render();
  </script>
</div>

API

Attributes

Name类型DefaultDescription
headingstringBefore you startCard title.
heading-level1-63Level of the title element (h1-h6), to fit the page outline.
durationtime ("15m", "1h30m", "90s", ms number)How long the flow itself takes with everything at hand. The total estimate adds the gathering time (data-prep-time) of every non-optional item that isn’t ready. Without it, only the gathering time is shown.
start-labelstringStartText of the Start button (“Start application”, “Start release”…).
start-hrefURLNavigates there once mv-start was not canceled (state is saved first). Without it, handle mv-start yourself.
storage-keystringOpt-in persistence: item states and the started flag are kept in localStorage under “mv-prep-list:<key>” and synced across tabs. Nothing is stored without it.
startedbooleanReflects (and sets) the started state: the card becomes a compact “Prepared” chip whose list opens on demand. Setting it directly emits no mv-start.
data-readinessready | partial | blockedSet by the component on the host (styleable): everything ready, some items missing or for later, or a required-to-start item missing.
data-prepstring (item id)On an authored child element: declares an item, the value is its id. Its first <strong>/<b>/heading (or [data-prep-label]) is the label; [data-prep-why] and [data-prep-where] children give the reason and where to find it (inline markup such as links is kept, moved as nodes, never re-parsed).
data-prep-kinddocument | number | access | device | decision | person | time | anyitemOn an item: icon and kind label. Other values are shown as-is with a neutral icon.
data-prep-timetimeOn an item: honest time to gather it when it isn’t at hand (“~10 min to get”), added to the total until it is ready.
data-prep-requirementrequired-to-start | needed | optionalneededOn an item: required-to-start blocks Start until ready (and cannot be deferred); needed can be marked “I’ll get it later”; optional never counts toward readiness.
data-prep-step / data-prep-hrefstring / URLOn an item: the flow step that uses it (“Step 4 · Income”), shown as “Used in …” and in the later note; with an href it becomes a deep link (mv-prep-goto).
data-prep-stateready | later | missingmissingOn an item: initial state (also what reset() returns to). Stored progress overrides it.

Properties

Name类型Description
readybooleanTrue when every non-optional item is ready (read-only).
canStartbooleanTrue when no required-to-start item is missing (read-only).
missingstring[]Ids of non-optional items not ready yet, missing or later (read-only).
laterstring[]Ids of items set aside for later: the flow should pause at their step (read-only).
statesRecord<string, "ready" | "later" | "missing">State of every item. Setting it applies the given entries silently (no event), e.g. from your server.
itemsArray<{ id, label, why?, where?, kind?, time?, requirement?, step?, href?, state? }>Read the items with their current state, or set them from data instead of markup (why/where as plain text). States of ids that still exist are kept.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (heading, start, later, whereLabel, requiredTag, optionalTag, usedIn, gather, laterNote, laterNoteNoStep, count, countLater, timeTotal, timeDetail, timeFlowOnly, timeGatherOnly, statusReady, statusPartial, statusBlocked, statusStarted, chipTitle, chipAll, oneItem, manyItems, underMinute, minutes, hours, hoursMinutes, announce*, kind*). English defaults.

Methods

NameDescription
getState(id) / isReady(id)State of one item (null if unknown) / whether it is ready. Use it in the flow to decide whether to pause at a step.
setItem(id, state)Changes one item from code (e.g. the document was uploaded in the flow) and emits mv-prep-change with source "api". Returns false if nothing changed.
start()Same as pressing Start: refused while blocked (returns false), otherwise emits the cancelable mv-start and switches to the started chip. Returns true once started.
reset()Back to the authored initial states and not started; clears the stored progress.
focusItem(id)Scrolls to an item, focuses its checkbox and highlights it briefly (opens the list when started). For a “Paused: you need …” message in the flow.
expand() / collapse()Opens or folds the list under the Prepared chip (started state).

Events

NameDescription
mv-prep-changeAn item changed. detail: { id, state, previous, source: "user" | "api" | "storage", ready, canStart, missing, later, counts: { ready, later, missing, total, blocking }, states }. For storage (another tab), id and state are null.
mv-startCancelable, when Start is pressed (or start() called) and nothing blocks. detail: { source, ready, canStart, missing, later, counts, states }. preventDefault() keeps the card as it is.
mv-prep-gotoCancelable, when an item’s “Used in …” deep link is followed. detail: { id, step, href, state }. preventDefault() stops the navigation (route it yourself).

Content structure

NameDescription
introChildren with slot="intro" are moved under the title (context, reassurance, a privacy note).
footerChildren with slot="footer" are placed before the Start button (e.g. a “Save and come back later” link).

CSS classes

NameDescription
mv-prep-list-cardThe card (<section> labelled by the title, a size container): -head, -icon, -heading, -intro, -overview (-time, -count, -meter with one -seg per item), -items, -foot.
mv-prep-list-itemOne row: data-state (ready | later | missing), data-requirement, data-kind, data-blocking, data-attention while highlighted. Parts: -check (native .mv-checkbox), -label, -tag, -meta (-kind, -gather, -step), -details (-why, -where), -note (later), -later (aria-pressed toggle).
mv-prep-list-status / mv-prep-list-startReadiness explanation (referenced by the Start button’s aria-describedby) and the Start .mv-button (aria-disabled while blocked).
mv-prep-list-chipCompact “Prepared” summary button shown once started (aria-expanded, controls the card): -chip-icon, -chip-title, -chip-meta, -chip-chevron.

CSS variables

NameDefaultDescription
--mv-prep-list-readyvar(--mv-success)Ready items: checkbox, meter segment, row tint and edge, chip icon.
--mv-prep-list-latervar(--mv-warning)Items for later: hatched meter segment and edge, pressed toggle, pause note.
--mv-prep-list-requiredvar(--mv-accent)“Needed to start” tag and the highlight shown when a blocked Start is pressed.

Accessibility

The card is a <section> labelled by its heading (level configurable). Each item is a real <input type="checkbox"> with a <label> (the item name) and aria-describedby pointing to its requirement tag (“Needed to start”, “Optional”), the reason, the where-to-find-it text and, when deferred, the later note (“We’ll pause at Step 4 · Income until you add it”). “I’ll get it later” is a toggle <button aria-pressed> whose accessible name includes the item (“I’ll get it later, Your bank account number”). The time estimate is plain text, never only a graphic; the segmented meter is aria-hidden and duplicated by the “5 of 7 ready · 1 for later” text. The Start button’s aria-describedby points to the status sentence, so its state is always explained in words; while a required-to-start item is missing it is aria-disabled (still focusable), and activating it moves focus to the first blocking checkbox, highlights it and announces what is needed instead of doing nothing. A polite live region gives a debounced count after ticking (“4 of 7 ready.”), announces when everything is ready or when Start becomes available, and confirms the start; after starting, focus moves to the “Prepared” chip, a button with aria-expanded controlling the card. States are never color-only: ready rows are checked and their details fold, later rows carry a hatched edge, a pressed toggle and a text note, blocking items a lock icon and a text tag. Ready details stay in the accessibility tree and unfold on focus-within. Reduced motion (OS or data-motion="reduce"): segments, folds and the chip appear without transition, the highlight is a static ring. Forced colors: the meter uses Highlight and hatched CanvasText, row edges and the pressed toggle stay visible.

本页面由 AI 翻译。报告翻译问题