익스클루시브

Lifelines <mv-lifelines>

어려운 단일 선택(플랜, 배송 옵션, 노트북 구성, 보험 등급, 인스턴스 크기)을 돕는 결정 도우미입니다. 각각 사용 횟수가 제한된 찬스이며, 사용하면 소진된 것이 눈에 보입니다.

문화적 레퍼런스

Who Wants to Be a Millionaire?, David Briggs, Mike Whitehill & Steven Knight (ITV) (1998, 게임 쇼). 막힌 참가자는 각 라이프라인을 한 번씩 사용할 수 있습니다. 하나는 오답 두 개를 제거하고, 하나는 스튜디오 방청객의 투표 결과를 보여 주며, 하나는 믿을 만한 친구에게 전화를 겁니다. UI에서는 같은 세 가지 도우미가 어려운 제품 선택을 돕습니다. 맞지 않는 옵션을 (이유와 함께) 제외하고, 비슷한 고객이 무엇을 골랐는지 보여 주며, 후보 목록을 동료에게 보내 투표를 받습니다. 각 도우미는 사용했음이 눈에 보이게 표시되고, 신뢰도 표시가 이를 종합합니다.

작동 방식

어려운 단일 선택(플랜, 배송 옵션, 노트북 구성, 보험 등급, 인스턴스 크기)을 위한 결정 도우미로, 각각은 사용 횟수가 제한되어 있고 사용하면 눈에 보이게 소진되는 라이프라인입니다. 직접 만든 네이티브 라디오나 옵션 카드를 감싸기만 하고 대체하지 않으므로, 폼 값, 유효성 검사, 화살표 키는 브라우저의 것 그대로입니다. “Narrow it down”은 narrow(context) 함수나 data-fit 점수를 사용해 사용자가 밝힌 요구에 분명히 맞지 않는 옵션을 제외합니다. 제외된 각 옵션에는 이유가 붙고, 현재 선택은 절대 숨겨지지 않으며, 최소 옵션 개수가 유지되고, “Show them”을 누르면 제외된 옵션이 다시 나타나 여전히 선택할 수 있습니다. “What others chose”는 data-share 속성이나 통계 객체 또는 함수를 바탕으로 모든 옵션에 백분율이 있는 분포 막대를 전체 기준과 “people like you” 세그먼트 기준으로 추가하고, 가장 많이 선택된 옵션을 표시합니다. “Ask someone”은 후보 목록과 현재 선택, 그리고 선택적 투표 링크를 담은 편집 가능한 메시지를 작성해 Web Share API로 보내거나 복사합니다. 답변은 addVote()로 돌아와 각 옵션에 표시됩니다. 신뢰도 표시는 현재 선택에 대한 적합도, 다수의 선택, 투표를 종합해 Strong / Moderate / Low 수준과 그 이유를 보여 줍니다. 모든 라이프라인은 취소 가능한 mv-lifeline 이벤트를 거치며, 이 이벤트의 waitUntil(promise)로 서버에서 적합도, 통계 또는 투표 링크를 가져올 수 있습니다.

카테고리폼
유형Web Component (<mv-lifelines>)
상태안정
함께 설치되는 항목button, textarea
Keywordsexclusive, culture, decision, choice, radio-group, option-cards, plan-picker, pricing, shipping, configurator, recommendation, social-proof, share, web-share, voting, confidence, filter, form

When to use

  • Users stall on a plan, tier or configuration picker and need help ruling out options before they commit
  • The app knows the user's needs (team size, deadline, budget) and can explain why an option doesn't fit
  • Showing what similar customers picked would reassure a hesitant buyer without pushing a single recommendation
  • A purchase is often decided with a colleague or partner, and the shortlist should be easy to send them for a vote

Avoid when

  • The choice is between two to five quick, low-stakes settings like view mode or alignment → use Segmented instead
  • The options only need to be listed as cards with a price and badge, with no decision help → use Radio instead
  • Plans must be compared feature by feature in a full pricing section with a billing toggle → use Pricing instead

설치

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

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

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

사용법

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

<div id="ll-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #ll-demo { display:grid; gap:1.25rem; align-content:start }
    #ll-demo .ll-layout { display:grid; grid-template-columns:minmax(0,1fr) 16.5rem; gap:1.25rem; align-items:start }
    #ll-demo .ll-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) }
    #ll-demo .ll-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:1.125rem 1.25rem 1rem; border-bottom:1px solid var(--mv-border) }
    #ll-demo .ll-head h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #ll-demo .ll-head p { margin:.1875rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ll-demo .ll-needs { display:flex; align-items:flex-end; gap:.75rem; flex-wrap:wrap }
    #ll-demo .ll-need { display:grid; gap:.3125rem; font-size:.75rem; font-weight:500; color:var(--mv-fg-muted) }
    #ll-demo .ll-need .mv-select { width:auto; min-width:7.5rem; height:var(--mv-control-sm); font-size:.8125rem }
    #ll-demo .ll-need .mv-choice { height:var(--mv-control-sm); align-items:center; font-size:.8125rem }
    #ll-demo .ll-body { padding:1.125rem 1.25rem 1.25rem }
    #ll-demo .mv-choice-group legend { font-size:.8125rem; color:var(--mv-fg-muted) }
    #ll-demo .mv-radio-meta { margin-inline-start:auto; padding-inline-start:.5rem; font-weight:600; font-size:.875rem; white-space:nowrap; font-variant-numeric:tabular-nums }
    #ll-demo .mv-radio-meta small { color:var(--mv-fg-muted); font-weight:500; font-size:.75rem }
    #ll-demo .ll-side { 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 }
    #ll-demo .ll-side h4 { display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin:0; font-size:.8125rem; font-weight:600 }
    #ll-demo .ll-side p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.45 }
    #ll-demo .ll-votes { display:grid; gap:.5rem }
    #ll-demo .ll-votes .mv-button { justify-content:flex-start; width:100% }
    #ll-demo .ll-sep { height:1px; background:var(--mv-border) }
    #ll-demo .ll-log { display:grid; gap:.3125rem; margin:0; padding:0; list-style:none; font:.6875rem/1.45 var(--mv-font-mono); color:var(--mv-fg-muted) }
    #ll-demo .ll-log li { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ll-demo .ll-log b { color:var(--mv-fg); font-weight:500 }
    #ll-demo .ll-ship .ll-body { display:grid; grid-template-columns:minmax(0,15rem) minmax(0,1fr); gap:1.5rem; align-items:start }
    #ll-demo .ll-order { display:grid; gap:.625rem; font-size:.8125rem }
    #ll-demo .ll-order dl { display:grid; gap:.375rem; margin:0 }
    #ll-demo .ll-order dl div { display:flex; justify-content:space-between; gap:.5rem }
    #ll-demo .ll-order dt { color:var(--mv-fg-muted) }
    #ll-demo .ll-order dd { margin:0; font-variant-numeric:tabular-nums }
    #ll-demo .ll-order .ll-by { display:flex; gap:.5rem; align-items:flex-start; padding:.625rem .75rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); color:var(--mv-fg-muted); font-size:.75rem; line-height:1.45 }
    #ll-demo .ll-order .ll-by b { color:var(--mv-fg); font-weight:600 }
    #ll-demo .ll-hint { margin:0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
    @media (max-width:54rem) {
      #ll-demo .ll-layout { grid-template-columns:minmax(0,1fr) }
      #ll-demo .ll-side { position:static }
      #ll-demo .ll-ship .ll-body { grid-template-columns:minmax(0,1fr) }
    }
  </style>

  <div class="ll-layout">
    <!-- Main: choosing a workspace plan, with the app's own narrow() and stats() -->
    <section class="ll-card" aria-labelledby="ll-plan-title">
      <header class="ll-head">
        <div>
          <h3 id="ll-plan-title">Choose a plan for Kestrel Studio</h3>
          <p>You can switch plans at any time. Prices billed yearly.</p>
        </div>
        <div class="ll-needs" role="group" aria-label="Your needs">
          <label class="ll-need">Team size
            <select class="mv-select" id="ll-seats">
              <option value="2">1-3 people</option>
              <option value="8" selected>4-10 people</option>
              <option value="18">11-25 people</option>
              <option value="60">26-100 people</option>
            </select>
          </label>
          <label class="ll-need">Storage
            <select class="mv-select" id="ll-storage">
              <option value="5">5 GB</option>
              <option value="50" selected>50 GB</option>
              <option value="500">500 GB</option>
            </select>
          </label>
          <span class="ll-need">Security
            <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ll-sso"> SSO required</label>
          </span>
        </div>
      </header>
      <div class="ll-body">
        <mv-lifelines id="ll-plan" similar-label="teams of 4-10"
          question="Which plan should we get for the design team? We’re 8 people."
          share-url="https://app.example.com/plans/vote?options={shortlist}&amp;pick={pick}">
          <fieldset class="mv-choice-group" data-layout="grid" style="--mv-choice-min:17rem">
            <legend>Plan</legend>
            <label class="mv-choice-card">
              <input type="radio" class="mv-radio" name="ll-plan" value="starter">
              <span class="mv-choice-text"><span class="mv-choice-title">Starter</span><span class="mv-choice-description">Up to 3 seats · 5 GB · Community support</span></span>
              <span class="mv-radio-meta">$0</span>
            </label>
            <label class="mv-choice-card">
              <input type="radio" class="mv-radio" name="ll-plan" value="team" checked>
              <span class="mv-choice-text"><span class="mv-choice-title">Team</span><span class="mv-choice-description">Up to 25 seats · 100 GB · Shared libraries</span></span>
              <span class="mv-radio-meta">$12 <small>/ seat</small></span>
            </label>
            <label class="mv-choice-card">
              <input type="radio" class="mv-radio" name="ll-plan" value="business">
              <span class="mv-choice-text"><span class="mv-choice-title">Business</span><span class="mv-choice-description">Unlimited seats · 1 TB · SSO and audit log</span></span>
              <span class="mv-radio-meta">$24 <small>/ seat</small></span>
            </label>
            <label class="mv-choice-card">
              <input type="radio" class="mv-radio" name="ll-plan" value="enterprise">
              <span class="mv-choice-text"><span class="mv-choice-title">Enterprise</span><span class="mv-choice-description">From 50 seats · SSO and SCIM · 99.99% SLA</span></span>
              <span class="mv-radio-meta">Custom</span>
            </label>
          </fieldset>
        </mv-lifelines>
      </div>
    </section>

    <aside class="ll-side" aria-label="Demo controls">
      <h4>Replies you received <span class="mv-badge" data-variant="secondary" data-shape="pill" id="ll-count">0 votes</span></h4>
      <p>Your app collects answers from the shared link and feeds each one back with <code>addVote()</code>.</p>
      <div class="ll-votes">
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" data-vote="team" data-from="Priya Raman" data-note="Team covers us, keep the budget for fonts.">Priya picks Team</button>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" data-vote="business" data-from="Marcus Olsen" data-note="We’ll want SSO once the agency joins.">Marcus picks Business</button>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" data-vote="team" data-from="Aiko Tanaka">Aiko picks Team</button>
      </div>
      <div class="ll-sep"></div>
      <button class="mv-button" data-variant="secondary" data-size="sm" type="button" id="ll-reset">Give lifelines back</button>
      <div class="ll-sep"></div>
      <h4>Events</h4>
      <ol class="ll-log" id="ll-log"><li>-</li></ol>
      <span class="mv-sr-only" role="status" id="ll-status"></span>
    </aside>
  </div>

  <!-- Variant: declarative data only (data-fit, data-share), two lifelines, no confidence readout -->
  <section class="ll-card ll-ship" aria-labelledby="ll-ship-title">
    <header class="ll-head">
      <div>
        <h3 id="ll-ship-title">Shipping</h3>
        <p>Order #48213 · 2 items · to Chicago, IL 60614</p>
      </div>
    </header>
    <div class="ll-body">
      <div class="ll-order">
        <dl>
          <div><dt>Trail runner, size 9</dt><dd>$128.00</dd></div>
          <div><dt>Merino socks, 2 pairs</dt><dd>$24.00</dd></div>
        </dl>
        <p class="ll-by"><span>You said it’s a gift for <b>Friday, October 2</b>. Options that won’t arrive in time can be set aside.</span></p>
      </div>
      <mv-lifelines lifelines="narrow, poll" threshold="0.7" show-confidence="false" similar-label="shoppers in Chicago" poll-note="Orders shipped to Illinois in the last 30 days." label="Not sure which one?">
        <fieldset class="mv-choice-group">
          <legend>Delivery</legend>
          <label class="mv-choice-card" data-fit="0" data-fit-reason="Arrives Oct 5-7, after Friday" data-share="38" data-share-similar="22">
            <input type="radio" class="mv-radio" name="ll-ship" value="economy">
            <span class="mv-choice-text"><span class="mv-choice-title">Economy</span><span class="mv-choice-description">6-8 business days</span></span>
            <span class="mv-radio-meta">Free</span>
          </label>
          <label class="mv-choice-card" data-fit="0.55" data-fit-reason="Arrives Sep 30 to Oct 2, cutting it close" data-share="41" data-share-similar="44">
            <input type="radio" class="mv-radio" name="ll-ship" value="standard" checked>
            <span class="mv-choice-text"><span class="mv-choice-title">Standard</span><span class="mv-choice-description">3-5 business days</span></span>
            <span class="mv-radio-meta">$6.99</span>
          </label>
          <label class="mv-choice-card" data-fit="1" data-fit-reason="Arrives Monday, Sep 28" data-share="16" data-share-similar="27">
            <input type="radio" class="mv-radio" name="ll-ship" value="express">
            <span class="mv-choice-text"><span class="mv-choice-title">Express</span><span class="mv-choice-description">2 business days, tracked</span></span>
            <span class="mv-radio-meta">$14.99</span>
          </label>
          <label class="mv-choice-card" data-fit="0.85" data-fit-reason="Ready Thursday, Oct 1 at Lakeview Market" data-share="5" data-share-similar="7">
            <input type="radio" class="mv-radio" name="ll-ship" value="pickup">
            <span class="mv-choice-text"><span class="mv-choice-title">Pickup point</span><span class="mv-choice-description">Collect from a locker near you</span></span>
            <span class="mv-radio-meta">$3.99</span>
          </label>
        </fieldset>
      </mv-lifelines>
    </div>
  </section>
  <p class="ll-hint">Each lifeline works once · “Narrow it down” was already used on the plan picker · change your needs to re-check the set-aside plans</p>

  <script type="module">
    await customElements.whenDefined("mv-lifelines");
    const $ = (id) => document.getElementById(id);
    const ll = $("ll-plan");
    const PLANS = {
      starter: { seats: 3, storage: 5, sso: false, min: 1 },
      team: { seats: 25, storage: 100, sso: false, min: 1 },
      business: { seats: Infinity, storage: 1000, sso: true, min: 1 },
      enterprise: { seats: Infinity, storage: 5000, sso: true, min: 50 },
    };
    const readNeeds = () => ({ seats: Number($("ll-seats").value), storage: Number($("ll-storage").value), sso: $("ll-sso").checked });
    const gb = (n) => (n >= 1000 ? `${n / 1000} TB` : `${n} GB`);

    // The app's own fit logic: 0..1 per option, with the reason shown to the user.
    ll.needs = readNeeds();
    ll.narrow = ({ needs, options }) => options.map(({ value }) => {
      const p = PLANS[value];
      if (p.seats < needs.seats) return { value, fit: 0.1, reason: `Up to ${p.seats} seats, you need ${needs.seats}` };
      if (p.min > needs.seats) return { value, fit: 0.2, reason: `Starts at ${p.min} seats, you need ${needs.seats}` };
      if (p.storage < needs.storage) return { value, fit: 0.15, reason: `${gb(p.storage)} of storage, you need ${gb(needs.storage)}` };
      if (needs.sso && !p.sso) return { value, fit: 0.15, reason: "No single sign-on" };
      if (!needs.sso && p.sso) return { value, fit: 0.65, reason: "Fits, but you’d pay for SSO you don’t need" };
      return { value, fit: 0.92, reason: `Room for ${Number.isFinite(p.seats) ? p.seats : "unlimited"} seats and ${gb(p.storage)}` };
    });
    // What others chose: overall and for teams of the same size.
    const SEGMENTS = {
      2: ["teams of 1-3", { starter: 71, team: 22, business: 6, enterprise: 1 }],
      8: ["teams of 4-10", { starter: 9, team: 58, business: 29, enterprise: 4 }],
      18: ["teams of 11-25", { starter: 2, team: 47, business: 43, enterprise: 8 }],
      60: ["teams of 26-100", { starter: 1, team: 12, business: 52, enterprise: 35 }],
    };
    ll.stats = ({ needs }) => {
      const [similarLabel, similar] = SEGMENTS[needs.seats];
      return {
        everyone: { starter: 41, team: 33, business: 19, enterprise: 7 },
        similar, similarLabel,
        note: "Based on 12,480 workspaces created in the last 90 days.",
      };
    };

    const log = $("ll-log");
    const push = (html) => {
      if (log.firstElementChild?.textContent === "-") log.replaceChildren();
      const li = document.createElement("li");
      const [name, rest] = html;
      li.append(Object.assign(document.createElement("b"), { textContent: name }), ` ${rest}`);
      log.prepend(li);
      while (log.children.length > 5) log.lastElementChild.remove();
    };
    ll.addEventListener("mv-lifeline", (e) => push(["mv-lifeline", `${e.detail.lifeline}${e.detail.refresh ? " (refresh)" : ""}`]));
    ll.addEventListener("mv-lifeline-result", (e) => push(["result", `${e.detail.lifeline} · ${e.detail.confidence?.level ?? "no pick"}`]));
    ll.addEventListener("mv-change", (e) => push(["mv-change", `${e.detail.value} · ${e.detail.confidence?.level ?? "-"}`]));
    ll.addEventListener("mv-share", (e) => push(["mv-share", e.detail.method]));
    ll.addEventListener("mv-vote", (e) => {
      push(["mv-vote", `${e.detail.vote.from} → ${e.detail.vote.value}`]);
      const n = e.detail.votes.length;
      $("ll-count").textContent = `${n} vote${n === 1 ? "" : "s"}`;
    });

    // Start with one lifeline already spent, so the mechanic is visible.
    ll.use("narrow");

    for (const id of ["ll-seats", "ll-storage", "ll-sso"]) {
      $(id).addEventListener("change", () => {
        ll.needs = readNeeds();
        ll.refresh();
      });
    }
    document.querySelectorAll("#ll-demo [data-vote]").forEach((btn) => {
      btn.addEventListener("click", () => {
        ll.addVote({ value: btn.dataset.vote, from: btn.dataset.from, note: btn.dataset.note });
        btn.disabled = true;
      });
    });
    $("ll-reset").addEventListener("click", async () => {
      await ll.reset();
      document.querySelectorAll("#ll-demo [data-vote]").forEach((b) => { b.disabled = false; });
      $("ll-count").textContent = "0 votes";
      $("ll-status").textContent = "All lifelines are available again.";
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
lifelinescomma list of narrow | poll | asknarrow, poll, askWhich lifelines are offered, in toolbar order.
usesnumber | "unlimited" | "narrow:1, poll:2, ask:unlimited"1How many times each lifeline can be used: one number for all, or per lifeline ("*" sets the default for the others). Up to five uses show as dots, more as a count.
thresholdnumber (0..1 or %)0.5Fit below which “Narrow it down” sets an option aside. Changing it re-runs a narrowing already done, without spending a use.
keepnumber2Minimum number of options left visible after narrowing. The best-fitting of the would-be removals are kept to reach it. The checked option is never set aside; if it fits poorly it is flagged “Might not fit” instead.
namestringName of the radio group to manage when the element contains several. Defaults to the name of the first radio inside.
labelstringNeed a hand deciding?Visible label of the lifelines toolbar (also its accessible name). An empty value hides it.
similar-labelstringpeople like youName of the “people like you” segment, written lowercase to read inside sentences (“Chosen by 58% of teams of 4-10”). It is capitalized where it starts a label. A stats object's similarLabel wins.
poll-notestringWhere the statistics come from, shown in the “What others chose” note (“Orders shipped to Illinois in the last 30 days.”). A stats object's note wins.
share-urlURL templateLink added to the “Ask someone” message, for example your vote page. {shortlist} (comma-separated values still visible) and {pick} are replaced, URI-encoded. A url resolved by waitUntil wins.
questionstringCould you help me decide? Here are the options I’m weighing:First line of the “Ask someone” message and title of the Web Share payload.
show-confidence"true" | "false"true"false" hides the confidence readout (the confidence property still works).
data-fit / data-fit-reasonnumber (0..1 or %) / stringOn an option (its label, a [data-option] wrapper or the radio): declarative fit score and the reason shown to the user. Used when narrow() is not set or returns nothing for that option.
data-share / data-share-similarnumberOn an option: share of everyone and of the “people like you” segment who chose it. Percentages, fractions (sum ≤ 1) or raw counts (sum above 100) are all normalized to percentages.
data-option / data-label / data-summaryattribute / string / stringOn an option: data-option marks the wrapper when it isn't the radio's <label>; data-label and data-summary override the name and summary used in announcements and in the “Ask someone” message (defaults: .mv-choice-title, then .mv-choice-description · .mv-radio-meta).
data-lifelines="toolbar | notes | readout"attributePlace on your own empty containers to choose where the toolbar, the notes and the readout go. By default the toolbar and notes are prepended and the readout appended.
data-lifeline-fit / data-lifeline-top / data-lifelines-hiddenset by the componentOn each option: fit | poor | out after narrowing, data-lifeline-top on the most chosen option, data-lifelines-hidden while an option is set aside and hidden (styleable).

Properties

Name유형Description
narrow(context) => fits | Promise<fits>Your fit logic. context: { needs, value, options: [{ value, label, summary, element, data }], setAside }. Return [{ value, fit, reason }] or { [value]: fit | { fit, reason } }, with fit in 0..1 (or a percentage).
statsobject | (context) => object | Promise<object>What others chose: { everyone: { [value]: share }, similar?: { [value]: share }, similarLabel?, everyoneLabel?, note? }, a flat { [value]: share } map, or [{ value, share, similar }]. Setting it after the lifeline was used updates the bars in place.
needsanyThe user's stated needs, passed to narrow() and stats() as context.needs. Call refresh() after changing it.
valuestringValue of the checked radio. Setting it checks the matching radio (showing it again if it was set aside) without emitting mv-change.
confidence{ value, level, label, pick, signals } | nullRead-only. value 0..1 (null without signals), level "high" (≥ 0.7) | "medium" (≥ 0.4) | "low", signals: [{ kind: "fit" | "crowd" | "votes", value, tone: "good" | "neutral" | "bad", text }]. Weighted mean: fit 0.5, crowd 0.25 (the pick's share relative to the most chosen), votes 0.25.
votes[{ value, from, note, at }]Answers received through addVote() (read-only copy).
setAsidestring[]Values currently set aside by “Narrow it down” (read-only).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (toolbarLabel, narrow, poll, ask, used, narrowTitle, narrowText, showAside, hideAside, tagOut, tagPoor, pollTitle, everyone, similar, mostChosen, mostChosenBy, askTitle, askText, share, copy, copied, question, askLeaning, askReply, askLink, answers, confidenceFor, levelHigh, levelMedium, levelLow, signalFit, signalCrowd, signalVotes, announce*…). English defaults.

Methods

NameDescription
use(name)Runs a lifeline ("narrow" | "poll" | "ask") as if its button was pressed. Resolves to true when it ran; for ask, when its panel opened. A lifeline with nothing to show (no data) is not spent.
usesLeft(name)Remaining uses of a lifeline (Infinity when unlimited).
refresh()Re-runs the lifelines already used (narrow and poll) against the current needs and data, without spending a use. mv-lifeline fires with refresh: true.
showAll(force?)Shows (true) or hides (false) the set-aside options; toggles without argument. Resolves to the new state.
addVote({ value, from?, note? })Feeds back an answer from someone you asked. A second vote from the same person replaces the first. Returns false for an unknown value.
clearVotes()Removes every vote.
reset()Gives every lifeline back and clears narrowing, statistics and votes; set-aside options come back.
openAsk() / closeAsk()Opens or closes the “Ask someone” panel.

Events

NameDescription
mv-lifelineCancelable, before a lifeline runs (for ask: when its panel opens for a new use). detail: { lifeline, refresh, usesLeft, context, waitUntil(promise) }. preventDefault() vetoes it (nothing is spent). Promises passed to waitUntil show a busy state; the last non-null value they resolve to is used as the data: fits for narrow, stats for poll, { text?, url? } for ask. A rejection spends nothing and emits mv-lifeline-error.
mv-lifeline-resultA lifeline ran. detail: { lifeline, refresh, result, confidence }. result: { setAside: [{ value, label, fit, reason }], fits } for narrow, { everyone, similar, top } for poll, { method, text, url } for ask.
mv-lifeline-errorA waitUntil promise, narrow() or stats() threw. detail: { lifeline, error }.
mv-changeThe user picked another option. detail: { value, label, confidence }. The native change event still bubbles from the radio.
mv-shareThe “Ask someone” message was shared or copied. detail: { method: "share" | "copy", text, url }. The first one of a use spends it.
mv-voteaddVote() recorded an answer. detail: { vote, votes, confidence }.

Content structure

NameDescription
(content)Your radio group: a <fieldset> of .mv-choice-card or .mv-choice labels, or any markup with native radios. Per-option results go into [data-lifelines-slot] if present, else .mv-choice-text, else the option wrapper.

CSS classes

NameDescription
mv-lifelines-toolbarLabelled group holding -toolbar-label and .mv-lifelines-buttons.
mv-lifelines-lifelineA lifeline button: -lifeline-icon, -lifeline-label, .mv-lifelines-uses (dots, count or “Used”). data-lifeline, data-state="available | busy | used", data-spent during the spend pulse.
mv-lifelines-note / mv-lifelines-askWhat each lifeline found, under the toolbar: -note-icon, -note-title, -note-text, -note-list, .mv-lifelines-legend. The ask panel adds .mv-lifelines-message (textarea), -ask-actions and .mv-lifelines-answers.
mv-lifelines-asidePer-option results (aria-hidden, linked to the radio with aria-describedby): .mv-lifelines-tag, .mv-lifelines-reason[data-kind], .mv-lifelines-poll > .mv-lifelines-row[data-segment] (-row-label, .mv-lifelines-meter, -row-value), .mv-lifelines-votes.
mv-lifelines-readoutConfidence readout, data-level="high | medium | low | none": -readout-title, .mv-lifelines-level, .mv-lifelines-gauge, .mv-lifelines-signals > li[data-tone].

CSS variables

NameDefaultDescription
--mv-lifelines-accentvar(--mv-accent)Lifeline icons, remaining-use dots, spend pulse and the “Moderate” level.
--mv-lifelines-everyone38% fg mixed into bgBar color of the “everyone” segment.
--mv-lifelines-similarvar(--mv-lifelines-accent)Bar color of the “people like you” segment.
--mv-lifelines-goodvar(--mv-success)Good-fit marks and the “Strong” level.
--mv-lifelines-badvar(--mv-warning)Poor-fit marks and the “Low” level.

Accessibility

The native radios are never replaced: the group keeps its <fieldset>/<legend>, arrow keys, required validation and form value. Lifelines are real <button>s in a group labelled by the visible toolbar label. Each button's name includes its state (“Narrow it down, 1 use left” / “, used” / “, working” with aria-busy). A spent lifeline keeps focus and gets aria-disabled="true"; pressing it announces that it was already used. The “Ask someone” button stays operable to reopen its panel, with aria-expanded and aria-controls. Results are announced once in a polite live region (“Set aside Starter and Enterprise. 2 options left. Confidence in Team: strong.”; “Most chosen by teams of 4-10: Team, 58%.”; “Priya Raman picked Team.”); selecting options never chatters. Per-option results are visible text, bars included (every bar has its percentage written next to it). They are aria-hidden inside the label so they don't lengthen the radio's name, and linked with aria-describedby, so a screen reader hears “Team, radio button, 2 of 4. Most chosen by teams of 4-10. Good fit: Room for 25 seats and 100 GB. Everyone 33%, teams of 4-10 58%”. Set-aside options leave the layout and the arrow-key cycle; the “Show them” toggle (aria-pressed, aria-controls pointing at the hidden options) brings them back selectable, marked “Set aside” with their reason, and the checked option is never hidden. The ask panel is a labelled region with a real <label> on its editable message; focus moves into it on open, and Escape or Close returns focus to the button. Share and copy results are announced, and if copying fails the message is selected for manual copy. Fit, crowd and confidence tones always come with an icon and words, never color alone. Reduced motion (OS or data-motion="reduce"): no FLIP, no bar growth, no spend pulse or busy blink. Forced colors: borders, use dots and bars fall back to system colors.

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