Exclusivebeta

Finger Hold — <mv-finger-hold>

Branching wizard where any decision page can be held with a “finger”: take an option, explore where it leads (next questions, price, resulting plan), then flip straight back to the held page exactly as you left it, fields included. Steps are plain child sections (data-step) whose choices point to other steps (data-goto), so the flow is a real graph, not a linear stepper. Up to 4 fingers stack as numbered, color-coded tabs on the top edge of the page with held leaves showing under it; a tab returns in one click (or 1–4, R), its × lets go, and “Keep this path” releases them all to commit what you are exploring. Everything visited is kept in an exploration tree: what you saw down an abandoned branch is written back on the choice that led there (“Peeked: Bronze HSA 3000 · $572/mo”, from any element named by outcome), and a path map draws the current path, held points, peeked branches and options not explored yet. Cancelable mv-navigate / mv-hold / mv-return / mv-complete events, per-step validation, optional History API sync (browser Back walks the flow) and English strings you can override.

CategoryNavigation
TypeWeb Component (<mv-finger-hold>)
Statusbeta
Also installsbutton, kbd
Keywordsexclusive, culture, wizard, branching, decision-tree, multi-step, onboarding, configurator, plan-selector, insurance, troubleshooting, compare, what-if, bookmark, path-map, history-api, keyboard

When to use

Avoid when

Install

node scripts/add.mjs finger-hold --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["finger-hold"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/finger-hold/finger-hold.js, components/finger-hold/finger-hold.css, components/button/button.css, components/kbd/kbd.css.

Usage

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

<div id="fh-demo" style="width:min(100%,62rem);margin-inline:auto">
  <style>
    #fh-demo { display:grid; gap:1.25rem; align-content:start }
    #fh-demo .fh-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap }
    #fh-demo .fh-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.015em }
    #fh-demo .fh-head p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:34rem }
    #fh-demo .fh-keys { display:flex; gap:.375rem 1rem; flex-wrap:wrap; color:var(--mv-fg-subtle); font-size:.75rem }
    #fh-demo .fh-keys span { display:inline-flex; align-items:center; gap:.375rem }
    #fh-demo [data-step] h4 { margin:0; font-size:1.0625rem; letter-spacing:-.01em; line-height:1.3 }
    #fh-demo [data-step] > p { margin:.375rem 0 1.125rem; color:var(--mv-fg-muted); font-size:.8125rem; max-width:36rem }
    #fh-demo .fh-plan-head { display:flex; align-items:center; gap:.625rem; flex-wrap:wrap }
    #fh-demo .fh-facts { display:grid; grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr)); gap:.5rem; margin:0 0 1rem; padding:0 }
    #fh-demo .fh-facts div { padding:.625rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle) }
    #fh-demo .fh-facts dt { color:var(--mv-fg-subtle); font-size:.6875rem; text-transform:uppercase; letter-spacing:.04em }
    #fh-demo .fh-facts dd { margin:.125rem 0 0; font-size:.875rem; font-weight:600; font-variant-numeric:tabular-nums }
    #fh-demo .fh-finish { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding-top:1rem; border-top:1px solid var(--mv-border) }
    #fh-demo .mv-choice { font-size:.8125rem }
    #fh-demo .fh-quote { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:.75rem 1.5rem; padding:.875rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #fh-demo .fh-quote-label { display:block; color:var(--mv-fg-subtle); font-size:.6875rem; text-transform:uppercase; letter-spacing:.05em }
    #fh-demo .fh-price { font-size:1.375rem; font-weight:650; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
    #fh-demo .fh-quote-note { color:var(--mv-fg-muted); font-size:.8125rem }
    #fh-demo .fh-log { margin:0; color:var(--mv-fg-subtle); font:.75rem/1.45 var(--mv-font-mono); min-height:1.1rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #fh-demo .fh-controls { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; justify-content:flex-end }
    #fh-demo .fh-variant { display:grid; grid-template-columns:minmax(0,15rem) minmax(0,1fr); gap:1.5rem; align-items:start; padding-top:.5rem }
    #fh-demo .fh-variant-copy h3 { margin:0; font-size:.9375rem }
    #fh-demo .fh-variant-copy p { margin:.375rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5 }
    #fh-demo .fh-steps { margin:.25rem 0 1rem; padding-left:1.125rem; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.6 }
    @media (max-width:44rem) {
      #fh-demo .fh-quote { grid-template-columns:1fr }
      #fh-demo .fh-controls { justify-content:flex-start }
      #fh-demo .fh-variant { grid-template-columns:1fr }
    }
  </style>

  <div class="fh-head">
    <div>
      <h3>Find your health plan</h3>
      <p>Hold a decision with a finger, peek at where an answer leads, then flip straight back and try the other one. Nothing you entered is lost.</p>
    </div>
    <div class="fh-keys" aria-hidden="true">
      <span><kbd class="mv-kbd">H</kbd> hold page</span>
      <span><kbd class="mv-kbd">R</kbd> return</span>
      <span><kbd class="mv-kbd">K</kbd> keep path</span>
      <span><kbd class="mv-kbd-group"><kbd class="mv-kbd">⇧</kbd><kbd class="mv-kbd">Enter</kbd></kbd> peek a choice</span>
    </div>
  </div>

  <!-- Main: a health-insurance plan finder with a live quote -->
  <mv-finger-hold id="fh-plans" label="Health plan finder" outcome="#fh-price">
    <section data-step="coverage" data-title="Coverage">
      <h4>Who needs coverage?</h4>
      <p>We size the plan to your household and estimate your monthly premium as you go.</p>
      <div class="mv-finger-hold-choices">
        <button type="button" class="mv-finger-hold-choice" data-goto="care" data-choice="me"><strong>Just me</strong><span>Individual coverage</span><span class="mv-finger-hold-choice-meta">1 person</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="care" data-choice="couple"><strong>Me and my partner</strong><span>Spouse or domestic partner</span><span class="mv-finger-hold-choice-meta">2 people</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="kids" data-choice="family"><strong>My family</strong><span>Partner and children under 26</span><span class="mv-finger-hold-choice-meta">3+ people</span></button>
      </div>
    </section>

    <section data-step="kids" data-title="Children">
      <h4>How many children are on the plan?</h4>
      <p>Children can stay on a family plan until they turn 26.</p>
      <div class="mv-finger-hold-choices">
        <button type="button" class="mv-finger-hold-choice" data-goto="care" data-choice="1"><strong>1 child</strong><span>3 people in total</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="care" data-choice="2"><strong>2 children</strong><span>4 people in total</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="care" data-choice="3"><strong>3 or more</strong><span>Premiums cap at 3 children</span></button>
      </div>
    </section>

    <section data-step="care" data-title="Care needs">
      <h4>How often do you see a doctor?</h4>
      <p>Think about the last twelve months, prescriptions included.</p>
      <div class="mv-finger-hold-choices">
        <button type="button" class="mv-finger-hold-choice" data-goto="deductible" data-choice="rarely"><strong>A checkup or two a year</strong><span>Mostly preventive care, rare prescriptions</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="network" data-choice="often"><strong>Regularly</strong><span>Ongoing prescriptions or a specialist</span></button>
      </div>
    </section>

    <section data-step="deductible" data-title="Deductible">
      <h4>Choose your deductible</h4>
      <p>A higher deductible lowers your premium, but you pay more out of pocket before coverage starts.</p>
      <div class="mv-finger-hold-choices">
        <button type="button" class="mv-finger-hold-choice" data-goto="silver" data-choice="low" data-label="$750 deductible"><strong>$750 deductible</strong><span>$30 copays from day one</span><span class="mv-finger-hold-choice-meta">Higher premium</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="bronze" data-choice="high" data-label="$3,000 deductible"><strong>$3,000 deductible</strong><span>Pairs with a tax-free HSA</span><span class="mv-finger-hold-choice-meta">Lowest premium</span></button>
      </div>
    </section>

    <section data-step="network" data-title="Doctors">
      <h4>Do you want to keep your current doctors?</h4>
      <p>Networks keep premiums down; open access costs more but never needs a referral.</p>
      <div class="mv-finger-hold-choices">
        <button type="button" class="mv-finger-hold-choice" data-goto="platinum" data-choice="any"><strong>Yes, any licensed doctor</strong><span>PPO: no referrals, out-of-network covered</span></button>
        <button type="button" class="mv-finger-hold-choice" data-goto="gold" data-choice="network"><strong>A network is fine</strong><span>HMO: pick a primary doctor, referrals for specialists</span></button>
      </div>
    </section>

    <section data-step="bronze" data-title="Bronze HSA 3000" data-end>
      <div class="fh-plan-head"><h4>Bronze HSA 3000</h4><span class="mv-badge" data-variant="secondary">Lowest premium</span></div>
      <p>Best if you rarely need care and want to save pre-tax dollars for the years you do.</p>
      <dl class="fh-facts">
        <div><dt>Deductible</dt><dd>$3,000</dd></div>
        <div><dt>Out-of-pocket max</dt><dd>$8,050</dd></div>
        <div><dt>Primary care</dt><dd>After deductible</dd></div>
      </dl>
      <div class="fh-finish">
        <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="dental" value="yes"> Add dental &amp; vision (+$24 per person)</label>
        <button type="button" class="mv-button" data-finish>Choose Bronze HSA</button>
      </div>
    </section>

    <section data-step="silver" data-title="Silver 750" data-end>
      <div class="fh-plan-head"><h4>Silver 750</h4><span class="mv-badge" data-variant="success">Most popular</span></div>
      <p>Predictable copays for everyday care, with a deductible you can actually reach.</p>
      <dl class="fh-facts">
        <div><dt>Deductible</dt><dd>$750</dd></div>
        <div><dt>Out-of-pocket max</dt><dd>$6,500</dd></div>
        <div><dt>Primary care</dt><dd>$30 copay</dd></div>
      </dl>
      <div class="fh-finish">
        <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="dental" value="yes"> Add dental &amp; vision (+$24 per person)</label>
        <button type="button" class="mv-button" data-finish>Choose Silver 750</button>
      </div>
    </section>

    <section data-step="gold" data-title="Gold HMO" data-end>
      <div class="fh-plan-head"><h4>Gold HMO</h4><span class="mv-badge" data-variant="info">Low out-of-pocket</span></div>
      <p>Low costs when you use care often, as long as you stay in the network.</p>
      <dl class="fh-facts">
        <div><dt>Deductible</dt><dd>$250</dd></div>
        <div><dt>Out-of-pocket max</dt><dd>$4,800</dd></div>
        <div><dt>Primary care</dt><dd>$20 copay</dd></div>
      </dl>
      <div class="fh-finish">
        <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="dental" value="yes"> Add dental &amp; vision (+$24 per person)</label>
        <button type="button" class="mv-button" data-finish>Choose Gold HMO</button>
      </div>
    </section>

    <section data-step="platinum" data-title="Platinum PPO" data-end>
      <div class="fh-plan-head"><h4>Platinum PPO</h4><span class="mv-badge" data-variant="outline">Any doctor</span></div>
      <p>No deductible and no referrals, in or out of network.</p>
      <dl class="fh-facts">
        <div><dt>Deductible</dt><dd>$0</dd></div>
        <div><dt>Out-of-pocket max</dt><dd>$3,500</dd></div>
        <div><dt>Any doctor</dt><dd>$15 copay</dd></div>
      </dl>
      <div class="fh-finish">
        <label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="dental" value="yes"> Add dental &amp; vision (+$24 per person)</label>
        <button type="button" class="mv-button" data-finish>Choose Platinum PPO</button>
      </div>
    </section>
  </mv-finger-hold>

  <div class="fh-quote">
    <div>
      <span class="fh-quote-label">Estimated premium</span>
      <span class="fh-price" id="fh-price">$778/mo</span>
    </div>
    <div style="min-width:0">
      <div class="fh-quote-note" id="fh-note">Silver 750 for 2 people</div>
      <p class="fh-log" id="fh-log" aria-live="polite">Ready.</p>
    </div>
    <div class="fh-controls">
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="fh-history"> Browser Back steps through the flow</label>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fh-restart">Start over</button>
    </div>
  </div>

  <!-- Variant: troubleshooting guide, no map, two fingers -->
  <div class="fh-variant">
    <div class="fh-variant-copy">
      <h3>Troubleshooting guide</h3>
      <p>Same mechanic without the map and with two fingers: support agents hold the symptom page and test each fix without losing their place.</p>
    </div>
    <mv-finger-hold id="fh-wifi" label="Wi-Fi troubleshooter" map="none" max-fingers="2">
      <section data-step="symptom" data-title="Symptom">
        <h4>What do you see on the laptop?</h4>
        <p>Open the Wi-Fi menu in the top-right corner of the screen.</p>
        <div class="mv-finger-hold-choices">
          <button type="button" class="mv-finger-hold-choice" data-goto="radio" data-choice="empty"><strong>No networks listed</strong><span>The list is empty or grayed out</span></button>
          <button type="button" class="mv-finger-hold-choice" data-goto="join" data-choice="listed"><strong>My network is listed</strong><span>It won’t join or keeps dropping</span></button>
        </div>
      </section>
      <section data-step="radio" data-title="Wireless radio">
        <h4>Is airplane mode on?</h4>
        <p>Look for the plane icon in the menu bar or in Quick Settings.</p>
        <div class="mv-finger-hold-choices">
          <button type="button" class="mv-finger-hold-choice" data-goto="fix-airplane" data-choice="yes"><strong>Yes</strong></button>
          <button type="button" class="mv-finger-hold-choice" data-goto="fix-driver" data-choice="no"><strong>No</strong></button>
        </div>
      </section>
      <section data-step="join" data-title="Joining">
        <h4>What happens when you pick the network?</h4>
        <p>Try once more and watch the status line under the network name.</p>
        <div class="mv-finger-hold-choices">
          <button type="button" class="mv-finger-hold-choice" data-goto="fix-password" data-choice="rejected"><strong>The password is rejected</strong></button>
          <button type="button" class="mv-finger-hold-choice" data-goto="fix-router" data-choice="timeout"><strong>It just times out</strong></button>
        </div>
      </section>
      <section data-step="fix-airplane" data-title="Turn off airplane mode" data-end>
        <h4>Turn off airplane mode</h4>
        <ol class="fh-steps"><li>Open Quick Settings.</li><li>Turn airplane mode off, then Wi-Fi on.</li><li>Wait ten seconds for the list to refresh.</li></ol>
        <button type="button" class="mv-button" data-size="sm" data-finish>This fixed it</button>
      </section>
      <section data-step="fix-driver" data-title="Reset the network adapter" data-end>
        <h4>Reset the network adapter</h4>
        <ol class="fh-steps"><li>Open Settings › Network › Advanced.</li><li>Choose “Network reset” and restart.</li><li>If the list stays empty, the adapter may need service.</li></ol>
        <button type="button" class="mv-button" data-size="sm" data-finish>This fixed it</button>
      </section>
      <section data-step="fix-password" data-title="Re-enter the password" data-end>
        <h4>Forget the network and re-enter the password</h4>
        <ol class="fh-steps"><li>Right-click the network and choose “Forget”.</li><li>Join again; the password is case-sensitive.</li><li>Check the label under the router for the default key.</li></ol>
        <button type="button" class="mv-button" data-size="sm" data-finish>This fixed it</button>
      </section>
      <section data-step="fix-router" data-title="Restart the router" data-end>
        <h4>Restart the router</h4>
        <ol class="fh-steps"><li>Unplug the router for 30 seconds.</li><li>Plug it back in and wait for a steady light.</li><li>Move within 30 feet and try again.</li></ol>
        <button type="button" class="mv-button" data-size="sm" data-finish>This fixed it</button>
      </section>
    </mv-finger-hold>
  </div>

  <script type="module">
    const fh = document.getElementById("fh-plans");
    const wifi = document.getElementById("fh-wifi");
    const price = document.getElementById("fh-price");
    const note = document.getElementById("fh-note");
    const log = document.getElementById("fh-log");
    const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });

    // A small, realistic pricing model: monthly premium per plan for one adult.
    const PLANS = { bronze: ["Bronze HSA 3000", 286], silver: ["Silver 750", 389], gold: ["Gold HMO", 452], platinum: ["Platinum PPO", 561] };
    const REACH = { coverage: Object.keys(PLANS), kids: Object.keys(PLANS), care: Object.keys(PLANS), deductible: ["silver", "bronze"], network: ["gold", "platinum"] };

    function household() {
      const who = fh.choices.find((c) => c.step === "coverage")?.choice;
      const kids = Number(fh.choices.find((c) => c.step === "kids")?.choice ?? 0);
      if (who === "couple") return { people: 2, factor: 2 };
      if (who === "family") return { people: 2 + Math.max(1, kids), factor: 2 + 0.6 * Math.max(1, kids) };
      return { people: 1, factor: 1 };
    }

    function quote() {
      const step = fh.step;
      const { people, factor } = household();
      const who = `${people} ${people === 1 ? "person" : "people"}`;
      if (PLANS[step]) {
        const [name, base] = PLANS[step];
        const dental = document.querySelector(`#fh-plans [data-step="${step}"] input[name="dental"]`)?.checked;
        price.textContent = `${usd.format(base * factor + (dental ? 24 * people : 0))}/mo`;
        note.textContent = `${name} for ${who}${dental ? " · dental & vision included" : ""}`;
      } else {
        const low = Math.min(...(REACH[step] ?? Object.keys(PLANS)).map((p) => PLANS[p][1]));
        price.textContent = `from ${usd.format(low * factor)}/mo`;
        note.textContent = fh.choices.length ? `Estimate for ${who}, narrowing down` : "Answer a few questions to see your price";
      }
    }

    const say = (text) => { log.textContent = text; };
    fh.addEventListener("mv-step", quote);
    fh.addEventListener("change", quote);
    fh.addEventListener("mv-hold", (e) => say(`mv-hold · finger ${e.detail.finger} on “${e.detail.label}”`));
    fh.addEventListener("mv-return", (e) => say(`mv-return · back to “${e.detail.label}”, peeked ${e.detail.peek.text}`));
    fh.addEventListener("mv-release", (e) => { if (e.detail.reason === "release" || e.detail.reason === "keep") say(`mv-release · “${e.detail.label}” (${e.detail.reason})`); });
    fh.addEventListener("mv-complete", (e) => {
      note.textContent = `Selected: ${e.detail.outcome.text}${e.detail.values.dental ? " with dental & vision" : ""}`;
      say(`mv-complete · ${e.detail.path.join(" › ")}`);
    });

    document.getElementById("fh-history").addEventListener("change", (e) => { fh.history = e.target.checked; });
    document.getElementById("fh-restart").addEventListener("click", () => { fh.reset(); quote(); say("Started over."); });

    await customElements.whenDefined("mv-finger-hold");
    // Start mid-exploration: two fingers held, one branch already peeked.
    fh.choose("couple");
    fh.hold();                 // finger 1 on "Care needs"
    fh.choose("rarely");
    fh.hold();                 // finger 2 on "Deductible"
    fh.choose("high");         // peek at the $3,000 plan…
    fh.returnTo(2);            // …flip back: the choice now reads "Peeked: Bronze HSA 3000 · $572/mo"
    fh.choose("low");          // and now exploring the $750 one
    quote();
    say("Two fingers held. Press R to flip back to Deductible.");

    wifi.hold();
    wifi.choose("listed");
  </script>
</div>

Cultural reference

Choose Your Own Adventure — Edward Packard & R. A. Montgomery (Bantam Books) (1979, book). Each page ends on a decision that sends the reader to another page, and readers keep a finger in the decision page to peek at where a choice leads, then flip back and choose differently. In the UI, any decision step can be held with a finger: the user explores an answer’s consequences, flips back to the held page with every answer intact, sees what they peeked written on the choice, and commits by letting go.

API

Attributes

NameTypeDefaultDescription
startstep idStep shown first. Defaults to the first data-step child.
max-fingersnumber (1–5)4How many decision pages can be held at once. When all are in use the “Hold this page” tab turns disabled and explains why; lowering it releases the deepest fingers.
outcomeCSS selectorElement whose text sums up the current result (a price, a plan name). Read when the user comes back up from a branch and written on the choice that led there as “Peeked: {step} · {outcome}”. Looked up in the current step, then the component, then the document. Without it, a step’s data-outcome or its title is used.
historybooleanSync with the History API: every step change pushes an entry (state only, the URL is untouched) and browser Back / Forward walk the exploration (a finger’s fields are restored when you land on its page, fingers off the new path are released). Other keys of history.state are preserved.
labelstringDecision flowAccessible name of the component (role="group").
mapauto | side | bottom | noneautoWhere the path map goes: auto puts it beside the page from 720px of component width, under it otherwise.
shortcuts"true" | "false"trueSingle-key shortcuts while focus is inside the component and not in a text field: H hold / release this page, R return to the latest finger, K keep this path, 1–5 return to that finger. Shift+Enter or Shift+click on a choice (hold, then choose) works either way.
data-stepstring (on a direct child)Declares a step. data-title sets its name (otherwise its first heading), data-end marks a result page (also implied when it has no choices), data-outcome gives a static outcome text, data-hold="false" prevents holding it. A step needs at least two choices to be holdable.
data-gotostep id (on a button or link inside a step)A choice leading to that step. data-choice is its id (defaults to the target, required when two choices share a target), data-label its short name (otherwise its first <strong> or its text), data-value what is reported in events. The step’s fields are validated before leaving unless data-novalidate is present.
data-finishmarker (inside a step)Completes the flow from this page: emits the cancelable mv-complete with the committed path and the named field values, then releases every finger.
data-mode / data-fingers / data-current-step / data-layout / data-completeset by the componentOn the host, for styling: free | exploring | here | done, number of fingers, current step id, side | bottom | none, present once completed. Choices get data-peeked when a branch behind them was explored; the visible step gets data-current.

Properties

NameTypeDescription
stepstringCurrent step id. Setting it navigates like go().
pathstring[]Step ids of the current path, start first (read-only).
choices{ step, choice, label, value, to }[]Decisions taken along the current path (read-only).
fingers{ number, step, label, depth, heldAt, here, peeks: { choice, label, outcome }[] }[]Held fingers, shallowest first, with what was peeked from each (read-only).
exploringbooleanTrue when at least one finger is held and the user is past it (read-only).
stepsstring[]Declared step ids in document order (read-only).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (keys: label, back, step, result, hold, holdFull, holdFullHint, exploring, heldHere, completed, returnTo, keep, tabs, tabReturn, tabHere, tabDescription, release, peeked, mapTitle, steps1, stepsN, held0, held1, heldN, mapHere, peeked1, peekedN, more1, moreN, mapItemHeld, mapItemPeeks, mapItemMore1, mapItemMoreN, legendPath, legendPeeked, legendOpen, legendHeld, announceStep, announceHold, announceReturn, announceReturnPeek, announceRelease, announceKeep, announceFull, announceComplete). English defaults.

Methods

NameDescription
choose(choice, { hold? })Takes a choice of the current step: its element, data-choice id or data-goto target. hold: true holds the page first. Returns false if refused.
go(stepId)Goes back up the path to that step when it is on it, or opens it as a new branch from the current page.
back()Previous step of the path (a finger on the page you leave is released).
hold()Holds the current page (emits the cancelable mv-hold). False when it is not a decision, is already held or all fingers are in use.
returnTo(n?)Flips back to finger n (1-based), default the latest one that is not the current page: restores the path and every field value as they were, releases the deeper fingers.
release(n?)Lets go of finger n (default the latest); the current path is kept.
keep()Releases every finger: the path being explored becomes the committed one.
complete()Same as a data-finish click, from code.
reset()Back to the start step, fingers released, exploration tree cleared, fields reset to their default values.
refresh()Re-reads steps and choices. Called automatically when children change (frameworks, CMS).

Events

NameDescription
mv-navigateCancelable, before any step change. detail: { from, to, reason: "choice" | "back" | "return" | "api" | "history", choice: { id, label, value } | null, path }. Not cancelable for history.
mv-stepAfter a step change (fields already restored on a return). Same detail as mv-navigate; reason "reset" after reset().
mv-holdCancelable, before a page is held. detail: { step, label, finger, fingers }.
mv-returnCancelable, before flipping back to a finger. detail: { step, label, finger, from, peek: { step, title, outcome, text } } where peek is what was on screen when the user left.
mv-releaseA finger was let go. detail: { step, label, finger, reason: "release" | "keep" | "return" | "back" | "complete" | "api" | "history" | "reset" }.
mv-completeCancelable, on data-finish or complete(). detail: { step, path, choices, values (named fields of the path’s steps, like FormData), outcome, released (fingers let go) }.

Content structure

NameDescription
(steps)Direct children with data-step. They stay where you put them (only hidden / shown), so framework-rendered children are safe; any other child flows under the book.

CSS classes

NameDescription
mv-finger-hold-choices / mv-finger-hold-choiceOptional choice-card grid and card (<strong> title, <span> details, .mv-finger-hold-choice-meta aside). Cards turn dashed once peeked.
mv-finger-hold-peekThe “Peeked: …” note appended inside a peeked choice (aria-hidden, linked through aria-describedby).
mv-finger-hold-tabs / -tab / -tab-num / -tab-label / -tab-return / -tab-release / -holdTab row on the top edge: one tab per finger (data-finger, data-here), and the dashed “Hold this page” ghost tab.
mv-finger-hold-bar / -back / -step / -pill / -return / -keepPage header: Back, step counter, exploring pill, Return and Keep this path.
mv-finger-hold-sheetThe page card; held fingers show as stacked leaves under it (box-shadow).
mv-finger-hold-map / -map-lines / -map-list / -map-legendPath map: SVG lanes (current path straight down, branches to the side, dashed stubs for options not explored) next to an ordered list of the path.

CSS variables

NameDefaultDescription
--mv-finger-hold-color-1 … -5accent, then hues turned around itColor of each finger: tab strip, number, leaf edge, map badge, exploring tint.
--mv-finger-hold-pathvar(--mv-accent)Current-path color in the map.
--mv-finger-hold-map-width15.5remWidth of the side map.

Accessibility

The host is a labelled group. Only the current step is rendered (others are hidden), and every user-initiated change moves focus to the new step’s heading (tabindex="-1", no ring for pointer users) and announces “Step 3: Deductible.” in a polite live region; holds, returns (“Back at Deductible, as you left it. You peeked: Bronze HSA 3000 · $572/mo.”), releases and completion are announced there too, never assertively. Fingers are real buttons in a labelled group: each tab is named “Return to Deductible”, described “Finger 2 of 4, held at step 3.”, carries aria-keyshortcuts, and becomes aria-current="step" + aria-disabled on the page you are on; each × is “Release the finger on Deductible”. The ghost tab “Hold this page” (aria-keyshortcuts H) turns aria-disabled with an explanation when all fingers are in use. Return and Keep this path are labelled buttons with shortcuts R and K; shortcuts only fire while focus is inside the component and never in text fields, and can be turned off. Focus never gets lost: when a tab or button disappears, it moves to the next logical control or the step heading. Peek notes are aria-hidden inside the choice but linked through aria-describedby, so the choice keeps its name and gains “Peeked: …” as description. The map’s SVG is aria-hidden; its content is an ordered list of the path in which each step reads its answer, “Held by finger 2.”, what was peeked and how many options were not explored. Colors always come with numbers, words and shapes (numbered badges, dashed = not explored / peeked). Reduced motion (OS or data-motion="reduce"): pages, tabs and leaves change without sliding or springing. Forced colors: tabs, badges and paths use system colors.