Exclusivobeta

Inside Count <mv-inside-count>

Pestañas, acordeones y páginas cerradas que dicen lo que contienen antes de abrirlas. Envuelve un contenedor (un formulario de ajustes en pestañas, un acordeón, un asistente por pasos, una barra lateral plegable, una tabla paginada) y cada región cerrada recibe en su activador un pequeño recuento con etiqueta, leído del propio contenido sin tener que marcar nada a mano: errores (aria-invalid, [data-issue], validación nativa de restricciones una vez que se ha modificado un campo o enviado su formulario), campos obligatorios aún vacíos, cambios sin guardar (value, checked o una selección distinta de su valor por defecto), elementos nuevos o no leídos ([data-new], [data-unread]), coincidencias de búsqueda para una consulta o un campo de búsqueda vinculado (sin distinguir acentos ni mayúsculas, resaltadas tanto en paneles abiertos como cerrados con la CSS Custom Highlight API), además de tipos personalizados mediante data-inside="kind" o reglas signals. Varios tipos comparten una píldora compacta en orden de prioridad (“1 error · 1 to fill · 2”), con un glifo por tipo; los modos compact y corner se adaptan a activadores pequeños como los números de página. Los adaptadores integrados cubren <details>, [role=tab] + aria-controls, los desplegables [aria-expanded] + aria-controls, los elementos de árbol con un grupo hijo, y data-inside-for para todo lo demás (una página, una columna desplazada fuera de su contenedor). Los recuentos se agregan (una sección cerrada suma sus subsecciones cerradas, incluidos los paneles de pestañas situados en otro lugar), se actualizan en vivo con input, change, invalid, reset y los cambios del DOM, y Alt+Shift+N o cualquier botón [data-inside-next] abre la siguiente región cerrada con errores, abriendo por el camino las anidadas, y enfoca el primer problema. reveal-on-submit corrige la validación nativa dentro de pestañas ocultas: la sección que contiene el primer campo inválido se abre para que el navegador pueda enfocarlo. markSaved() fija los valores actuales como estado limpio.

CategoríaNavegación
TipoWeb Component (<mv-inside-count>)
Estadobeta
KitFormularios que no pierden a nadie
Keywordsexclusive, culture, tabs, accordion, details, stepper, pagination, sidebar, disclosure, validation, errors, required, unsaved-changes, dirty, unread, search, search-highlight, custom-highlight-api, count, roll-up, next-issue, keyboard, aria-describedby, forms

When to use

  • A settings or profile form is split into tabs and errors, empty required fields or unsaved edits must not hide in a tab that is not selected
  • A search box filters a tabbed or sectioned page and each closed section should say how many matches it holds
  • A paginated review (import rows, moderation queue) must show which other pages still hold errors or duplicates
  • Native form validation silently fails because the first invalid field sits in a hidden tab or collapsed section

Avoid when

  • Sections nest several levels deep and the depth of each issue plus a path-opening walk-through matter most → use Pea instead
  • A tab only needs a fixed number the app already knows (12 notifications) → use Badge instead
  • The page must warn before leaving with unsaved work, not locate it → use Tell-Tale instead

Instalación

node scripts/add.mjs inside-count --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["inside-count"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/inside-count/inside-count.js, components/inside-count/inside-count.css.

Uso

Inicio rápido, el marcado mínimo que funciona:

<mv-inside-count>
  <details><summary>Billing</summary><label>VAT number <input name="vat" required></label></details>
  <details><summary>Profile</summary><label>Name <input name="name" value="Ada"></label></details>
</mv-inside-count>

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:

<div id="ic-demo" style="width:min(100%,54rem);margin-inline:auto">
  <style>
    #ic-demo { display:grid; gap:1.25rem; align-content:start; font-size:.875rem }
    #ic-demo .ic-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #ic-demo .ic-head { display:flex; align-items:center; gap:.75rem 1rem; flex-wrap:wrap; padding:.875rem 1.125rem; border-bottom:1px solid var(--mv-border) }
    #ic-demo .ic-title { display:grid; gap:.125rem; flex:1 1 12rem; min-width:0 }
    #ic-demo .ic-title strong { font-size:.9375rem; letter-spacing:-.01em }
    #ic-demo .ic-title span { color:var(--mv-fg-muted); font-size:.8125rem }
    #ic-demo .ic-search { width:13rem; max-width:100% }
    #ic-demo .ic-tools { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #ic-demo .ic-body { padding:1rem 1.125rem 1.125rem }
    #ic-demo mv-tabs [role="tabpanel"] { padding-top:.25rem }
    #ic-demo .ic-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr)); gap:.875rem 1rem }
    #ic-demo .ic-note { margin:.875rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5 }
    #ic-demo .ic-members { display:grid; margin:0; padding:0; list-style:none; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg) }
    #ic-demo .ic-members li { display:flex; align-items:center; gap:.75rem; padding:.625rem .75rem; font-size:.8125rem }
    #ic-demo .ic-members li + li { border-top:1px solid var(--mv-border) }
    #ic-demo .ic-av { display:grid; place-items:center; width:1.875rem; height:1.875rem; border-radius:50%; background:var(--mv-bg-emphasis); font-size:.6875rem; font-weight:600; flex-shrink:0 }
    #ic-demo .ic-who { display:grid; flex:1; min-width:0 }
    #ic-demo .ic-who span { color:var(--mv-fg-muted); font-size:.75rem }
    #ic-demo .ic-switches { display:grid; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg) }
    #ic-demo .ic-switches .mv-choice { padding:.75rem .875rem }
    #ic-demo .ic-switches .mv-choice + .mv-choice { border-top:1px solid var(--mv-border) }
    #ic-demo .ic-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.75rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #ic-demo .ic-status { margin:0; color:var(--mv-fg-muted); font-size:.75rem; flex:1 1 16rem }
    #ic-demo .ic-status b { color:var(--mv-fg); font-weight:600 }
    #ic-demo .ic-opts { display:flex; align-items:center; gap:.5rem 1rem; flex-wrap:wrap; font-size:.8125rem }
    #ic-demo .ic-rows { display:grid; margin:0; padding:0; list-style:none }
    #ic-demo .ic-rows li { display:grid; grid-template-columns:2rem minmax(0,1.1fr) minmax(0,1.4fr) 9rem; align-items:center; gap:.75rem; padding:.5rem 1.125rem; font-size:.8125rem; border-bottom:1px solid var(--mv-border) }
    #ic-demo .ic-rows li > .mv-badge { justify-self:end }
    #ic-demo .ic-rows li > span:first-child { color:var(--mv-fg-subtle); font-variant-numeric:tabular-nums }
    #ic-demo .ic-rows li > span:nth-child(3) { color:var(--mv-fg-muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ic-demo .ic-pager { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; padding:.75rem 1.125rem 1rem }
    #ic-demo .ic-pager .mv-pagination ul { gap:.625rem }
    #ic-demo kbd { font-size:.6875rem }
    #ic-demo mv-tabs [role="tablist"] { overflow-x:auto; scrollbar-width:none }
    @media (max-width:40rem) { #ic-demo .ic-rows li { grid-template-columns:1.5rem minmax(0,1fr) auto } #ic-demo .ic-rows li > span:nth-child(3) { display:none } }
  </style>

  <!-- 1 · Settings form: every closed tab (and the closed “Tax details” section inside Billing) tells what it holds -->
  <mv-inside-count id="ic-settings" query-from="#ic-search">
    <form class="ic-card" id="ic-form" aria-labelledby="ic-form-title">
      <div class="ic-head">
        <span class="ic-title">
          <strong id="ic-form-title">Workspace settings</strong>
          <span>Northwind Labs · Business plan</span>
        </span>
        <span class="ic-tools">
          <span class="mv-input-group ic-search" data-size="sm">
            <label class="mv-input-group-addon" for="ic-search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="mv-sr-only">Search settings</span></label>
            <input class="mv-input" id="ic-search" type="search" value="invoice" placeholder="Search settings" autocomplete="off">
          </span>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm" data-inside-next>
            Next issue <kbd class="mv-kbd-group"><kbd class="mv-kbd">Alt</kbd><kbd class="mv-kbd">⇧</kbd><kbd class="mv-kbd">N</kbd></kbd>
          </button>
          <button type="submit" class="mv-button" data-size="sm">Save changes</button>
        </span>
      </div>

      <div class="ic-body">
        <mv-tabs data-variant="underline" value="general">
          <div role="tablist" aria-label="Settings sections">
            <button role="tab" data-value="general">General</button>
            <button role="tab" data-value="billing">Billing</button>
            <button role="tab" data-value="team">Team</button>
            <button role="tab" data-value="notifications">Notifications</button>
          </div>

          <div role="tabpanel" data-value="general">
            <div class="ic-grid">
              <div class="mv-field">
                <label class="mv-label" for="ic-name" data-required>Workspace name</label>
                <input class="mv-input" id="ic-name" name="name" value="Northwind Labs" required>
              </div>
              <div class="mv-field">
                <label class="mv-label" for="ic-tz">Time zone</label>
                <select class="mv-input" id="ic-tz" name="tz">
                  <option>America/New_York (UTC−04:00)</option>
                  <option>Europe/London (UTC+01:00)</option>
                  <option>Asia/Tokyo (UTC+09:00)</option>
                </select>
              </div>
            </div>
            <p class="ic-note">Type in a field of another tab, clear a required one, or search: the closed tabs update as you go.</p>
          </div>

          <div role="tabpanel" data-value="billing">
            <div class="ic-grid">
              <div class="mv-field">
                <label class="mv-label" for="ic-bill-email" data-required>Billing email</label>
                <input class="mv-input" id="ic-bill-email" name="billingEmail" type="email" required placeholder="[email protected]">
              </div>
              <div class="mv-field">
                <label class="mv-label" for="ic-prefix">Invoice prefix</label>
                <input class="mv-input" id="ic-prefix" name="prefix" value="NWL-">
              </div>
            </div>
            <div class="mv-accordion" data-variant="bordered" style="margin-top:1rem">
              <details class="mv-accordion-item">
                <summary>Tax details</summary>
                <div class="mv-accordion-content">
                  <div class="mv-field">
                    <label class="mv-label" for="ic-vat">VAT number</label>
                    <input class="mv-input" id="ic-vat" name="vat" value="GB12345" pattern="GB\d{9}" aria-invalid="true" aria-describedby="ic-vat-err">
                    <p class="mv-field-error" id="ic-vat-err">A UK VAT number is GB followed by 9 digits, for example GB123456789.</p>
                  </div>
                </div>
              </details>
            </div>
            <p class="ic-note">Invoices are emailed on the 1st of each month, in USD.</p>
          </div>

          <div role="tabpanel" data-value="team">
            <ul class="ic-members">
              <li><span class="ic-av" aria-hidden="true">EC</span><span class="ic-who"><strong>Emma Clarke</strong><span>Owner · [email protected]</span></span></li>
              <li data-new><span class="ic-av" aria-hidden="true">KT</span><span class="ic-who"><strong>Kenji Tanaka</strong><span>Invited Sep 22 · [email protected]</span></span><span class="mv-badge" data-variant="info">New</span></li>
              <li data-new><span class="ic-av" aria-hidden="true">AO</span><span class="ic-who"><strong>Amara Okafor</strong><span>Invited Sep 23 · [email protected]</span></span><span class="mv-badge" data-variant="info">New</span></li>
            </ul>
          </div>

          <div role="tabpanel" data-value="notifications">
            <div class="ic-switches">
              <label class="mv-choice" data-control="end"><input type="checkbox" role="switch" class="mv-switch" name="nPaid" checked><span class="mv-choice-text"><span class="mv-choice-title">Invoice paid</span><span class="mv-choice-description">Email the billing contact when a payment clears</span></span></label>
              <label class="mv-choice" data-control="end"><input type="checkbox" role="switch" class="mv-switch" name="nWeekly" id="ic-weekly"><span class="mv-choice-text"><span class="mv-choice-title">Weekly usage report</span><span class="mv-choice-description">Every Monday at 9:00 a.m.</span></span></label>
              <label class="mv-choice" data-control="end"><input type="checkbox" role="switch" class="mv-switch" name="nSecurity" checked><span class="mv-choice-text"><span class="mv-choice-title">Security alerts</span><span class="mv-choice-description">New sign-ins and API key changes</span></span></label>
            </div>
          </div>
        </mv-tabs>
      </div>

      <div class="ic-foot">
        <p class="ic-status" id="ic-status" aria-live="off">…</p>
        <span class="ic-opts">
          <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ic-compact" data-inside-ignore> Compact counts</label>
          <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ic-reveal" data-inside-ignore checked> Open first error on save</label>
        </span>
      </div>
    </form>
  </mv-inside-count>

  <!-- 2 · Paginated import review: page numbers carry a corner count of what the other pages hold -->
  <mv-inside-count id="ic-import" style="--mv-inside-count-duplicate:var(--mv-warning)" display="corner" kinds="error duplicate" next-kinds="error duplicate" shortcut="none">
    <section class="ic-card" aria-labelledby="ic-import-title">
      <div class="ic-head">
        <span class="ic-title">
          <strong id="ic-import-title">Review contacts import</strong>
          <span>contacts-sept.csv · 12 rows · 3 pages</span>
        </span>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" data-inside-next>Show next page with issues</button>
      </div>
      <div id="ic-p1">
        <ul class="ic-rows">
          <li><span>1</span><strong>Lucas Silva</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
          <li><span>2</span><strong>Sofia Rossi</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
          <li data-issue><span>3</span><strong>Omar Haddad</strong><span>omar.haddad@</span><span class="mv-badge" data-variant="danger">Invalid email</span></li>
          <li><span>4</span><strong>Priya Natarajan</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
        </ul>
      </div>
      <div id="ic-p2" hidden>
        <ul class="ic-rows">
          <li data-issue><span>5</span><strong>Chen Wei</strong><span>-</span><span class="mv-badge" data-variant="danger">Missing email</span></li>
          <li><span>6</span><strong>Hannah Becker</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
          <li data-inside="duplicate"><span>7</span><strong>Sofia Rossi</strong><span>[email protected]</span><span class="mv-badge" data-variant="warning">Duplicate of row 2</span></li>
          <li data-issue><span>8</span><strong>Mateo García</strong><span>mateo@garcia</span><span class="mv-badge" data-variant="danger">Invalid email</span></li>
        </ul>
      </div>
      <div id="ic-p3" hidden>
        <ul class="ic-rows">
          <li><span>9</span><strong>Aisha Bello</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
          <li data-inside="duplicate"><span>10</span><strong>Lucas Silva</strong><span>[email protected]</span><span class="mv-badge" data-variant="warning">Duplicate of row 1</span></li>
          <li><span>11</span><strong>Noah Williams</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
          <li><span>12</span><strong>Yuki Sato</strong><span>[email protected]</span><span class="mv-badge" data-variant="success">Ready</span></li>
        </ul>
      </div>
      <div class="ic-pager">
        <nav class="mv-pagination" aria-label="Import pages">
          <ul id="ic-pages">
            <li><button type="button" class="mv-pagination-item" data-inside-for="ic-p1" data-page="1" aria-current="page">1</button></li>
            <li><button type="button" class="mv-pagination-item" data-inside-for="ic-p2" data-page="2">2</button></li>
            <li><button type="button" class="mv-pagination-item" data-inside-for="ic-p3" data-page="3">3</button></li>
          </ul>
        </nav>
        <p class="ic-status" id="ic-import-status" style="flex:0 1 auto">…</p>
      </div>
    </section>
  </mv-inside-count>

  <script type="module">
    await Promise.all(["mv-inside-count", "mv-tabs"].map((t) => customElements.whenDefined(t)));
    const settings = document.getElementById("ic-settings");
    const status = document.getElementById("ic-status");
    const form = document.getElementById("ic-form");

    // A switch toggled earlier in this session: unsaved, in a closed tab.
    document.getElementById("ic-weekly").checked = true;
    settings.refresh();

    const phrase = (totals, strings) => {
      const parts = Object.entries(totals).map(([k, n]) => {
        const [one, many = one] = String(strings[`${k}Long`] ?? strings[k] ?? k).split("|");
        return `${n} ${n === 1 ? one : many}`;
      });
      return parts;
    };
    const renderStatus = (totals) => {
      const parts = phrase(totals, settings.strings);
      status.replaceChildren();
      if (!parts.length) { status.textContent = "Nothing hidden in closed sections."; return; }
      const b = document.createElement("b");
      b.textContent = "In closed sections: ";
      status.append(b, parts.join(" · "));
    };
    settings.addEventListener("mv-inside-change", (e) => renderStatus(e.detail.totals));
    renderStatus(settings.totals);

    form.addEventListener("submit", (e) => {
      e.preventDefault();
      settings.markSaved();
      status.textContent = `Saved at ${new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}.`;
    });
    const compact = document.getElementById("ic-compact");
    compact.addEventListener("change", () => { settings.display = compact.checked ? "compact" : "full"; });
    // Narrow screens: glyphs and numbers only, so the four tabs still fit.
    if (matchMedia("(max-width: 40rem)").matches) { compact.checked = true; settings.display = "compact"; }
    document.getElementById("ic-reveal").addEventListener("change", (e) => { settings.revealOnSubmit = e.target.checked; });
    settings.revealOnSubmit = true;

    // Import review: plain page switching, the component reads what each page holds.
    const imp = document.getElementById("ic-import");
    const impStatus = document.getElementById("ic-import-status");
    imp.strings = { duplicate: "duplicate|duplicates", duplicateLong: "duplicate row|duplicate rows" };
    const pages = document.getElementById("ic-pages");
    pages.addEventListener("click", (e) => {
      const btn = e.target.closest("[data-page]");
      if (!btn) return;
      for (const b of pages.querySelectorAll("[data-page]")) {
        const on = b === btn;
        if (on) b.setAttribute("aria-current", "page"); else b.removeAttribute("aria-current");
        document.getElementById(b.getAttribute("data-inside-for")).hidden = !on;
      }
    });
    const impRender = (totals) => {
      const parts = phrase(totals, imp.strings);
      impStatus.textContent = parts.length ? `Other pages: ${parts.join(" · ")}` : "No issues on other pages.";
    };
    imp.addEventListener("mv-inside-change", (e) => impRender(e.detail.totals));
    impRender(imp.totals);
  </script>
</div>

Referencia cultural

Buscaminas, Microsoft (Robert Donner & Curt Johnson, a partir de juegos anteriores de búsqueda de minas) (1990, videojuego). Cada número del tablero indica cuántas minas se esconden en las casillas cerradas que lo rodean, así que el jugador decide dónde abrir sin abrirlo todo. En la interfaz, cada pestaña, sección o página cerrada muestra en su activador cuántos errores, campos obligatorios vacíos, cambios sin guardar, elementos nuevos o coincidencias de búsqueda oculta, y un atajo abre la siguiente que requiere atención.

API

Attributes

NameTipoDefaultDescription
kindsspace-separated listerror required change new matchKinds shown, in priority order (first = most important, sets the pill tint and comes first). Kinds found in the content but not listed (custom data-inside kinds, signals) are appended after.
querystringSearch text counted as match in closed regions and highlighted everywhere inside the component (case- and accent-insensitive). Ignored while query-from is bound.
query-fromCSS selectorAn input (inside or outside the component) whose value is used as the query, live on every input event. The field itself is never counted as a change.
min-querynumber2Shortest query that is searched.
displayfull | compact | cornerfullfull: glyph, number and word for the first limit kinds, glyph and number for the others. compact: glyph and number only. corner: a small counter pinned on the trigger’s top-end corner with the top kind only (page numbers, icon buttons). data-inside-display on a trigger overrides it.
limitnumber2In full mode, how many kinds get a word (“2 errors”); the rest keep glyph and number.
showclosed | alwaysclosedalways also keeps a quieter pill on open triggers.
shortcutstring | noneAlt+Shift+NKey combination (focus inside the component) that opens the next closed region holding next-kinds and focuses its first issue. Matched on the key or the physical key code, so it works whatever the layout; none disables it.
next-kindsspace-separated listerror requiredKinds that make a region a target for the shortcut, [data-inside-next] buttons and openNext().
reveal-on-submitbooleanWhen a submission finds its first invalid field inside a closed region, open the regions on its path (cancelable mv-inside-open, source "submit") so the browser can focus it and show its message, as it would for a visible field.
data-inside-forids | CSS selectorOn a custom trigger (page number, step, column toggle): the panel(s) it stands for. Closed when every panel is hidden, inert, not rendered or scrolled entirely out of its scroll container; data-inside-closed="true | false" on the trigger forces the state.
data-insidespace-separated kindsOn any element: counts it as one signal of each kind (duplicate, warning, mention…). Name them with strings ("duplicate|duplicates") and tint them with --mv-inside-count-<kind>.
data-issue / data-new / data-unread / data-changedboolean | "false"On any element: counted as an error, a new item or an unsaved change. "false", "off" or "none" ignores it.
data-inside-ignorebooleanNothing inside is counted or highlighted; on a trigger, it is not a region.
data-inside-slotbooleanInside a trigger: where the pill is placed (default: appended to the trigger).
data-inside-labelstringOn a trigger: name used in announcements and events (default: aria-label, then its text).
data-inside-kindkindSet by the component on each trigger showing a pill: its most important kind. Style your own triggers with it.
data-inside-nextbooleanOn a button inside the component: opens the next region with issues. Gets aria-disabled when there is none, data-inside-pending (number of such regions) and aria-keyshortcuts.

Properties

NameTipoDescription
regionsArray<{ trigger, panels, closed, label, counts, total, depth }>Every region found, in document order, with its rolled-up counts (read-only snapshot).
totalsRecord<kind, number>Signals hidden in closed regions, each counted once (read-only).
signalsArray<{ kind, selector?, test?(el) }>Custom rules: every element of a region matching selector (and test) counts as one signal of kind.
stringsPartial<Record<string, string>>Overrides: <kind> (pill word, "singular|plural"), <kind>Long (description word), describe ({list}), hint ({shortcut}), announce ({count}, {where}), announceClear. English defaults; lists are joined with Intl.ListFormat in the nearest lang.
kinds / query / queryFrom / minQuery / display / limit / show / shortcut / nextKinds / revealOnSubmitreflectedMirror the attributes.

Methods

NameDescription
openNext({ kinds?, focus = true })Opens the next closed region holding kinds (default next-kinds) after the focused section, wrapping around, opens closed regions nested on the way, then focuses and scrolls to the first issue (a temporary tabindex="-1" for non-focusable rows). Returns Promise<Element | null>.
markSaved(root?)Commits the current values of the fields in root (default: the whole component) as their defaults, so they stop counting as changes and a form reset returns to them. Clears data-changed. Returns how many fields were dirty.
refresh()Recomputes synchronously (normally batched per frame; needed after setting .value or .checked from code, which fires no event). Returns totals.
countsFor(trigger)Counts of one region (element or selector), or null.

Events

NameDescription
mv-inside-changeCounts or open states changed (also once on connection). detail: { regions, totals, reason: "init" | "input" | "submit" | "query" | "toggle" | "mutation" | "scroll" | "api" }.
mv-inside-openCancelable, before the component opens a region (shortcut, button, openNext or reveal-on-submit). detail: { trigger, panels, counts, label, source: "shortcut" | "button" | "api" | "submit" }. preventDefault() keeps it closed; open it your way if your tabs are state-driven.

Content structure

NameDescription
(content)Any structure with tabs, <details>, disclosures, tree items or data-inside-for triggers. The component only adds a pill (aria-hidden, data-inside-own) inside closed triggers, an id to their aria-describedby, data-inside-kind, and a hidden description container; everything else stays yours.

CSS classes

NameDescription
mv-inside-countThe pill on a trigger (aria-hidden). data-kind = most important kind, data-display, data-state="closed | open".
mv-inside-count-segOne kind in the pill: .mv-inside-count-icon (glyph drawn with a mask), .mv-inside-count-num, .mv-inside-count-word (full mode). data-kind.

CSS variables

NameDefaultDescription
--mv-inside-count-errorvar(--mv-danger)Tint of errors (exclamation glyph).
--mv-inside-count-requiredvar(--mv-fg-muted)Tint of required fields left empty (asterisk).
--mv-inside-count-changevar(--mv-info)Tint of unsaved changes (pencil).
--mv-inside-count-newvar(--mv-accent)Tint of new or unread items (spark).
--mv-inside-count-matchvar(--mv-warning)Tint of search matches (magnifier). Custom kinds read --mv-inside-count-<kind> (ring glyph).
--mv-inside-count-match-bgsoft yellow (light) / amber 40% (dark)Background of highlighted search matches (::highlight(mv-inside-match)).

Accessibility

Nothing relies on color: each kind has its own glyph next to its number, and in full mode a word (“2 errors”). The pill is aria-hidden so it never changes the trigger’s accessible name; instead the trigger’s aria-describedby gains a hidden description (“Contains 1 error, 1 required field left empty and 2 search matches. Press Alt+Shift+N to open it.”), removed as soon as the region opens or empties, so a screen reader hears it when focusing the tab, summary or button. Focus is never moved on its own: counts update silently while typing, and the only announcement is a polite one after a submission, and only when the errors hidden in closed sections changed (“2 errors in closed sections: Billing (1) and Tax details (1).”, then “No errors left in closed sections.”). Focus moves only on an explicit request: the shortcut or a [data-inside-next] button (aria-keyshortcuts, aria-disabled when nothing is left) opens the next region and focuses its first issue, and reveal-on-submit opens the path to the first invalid field so the browser focuses it, exactly as native validation does for a visible field. Search matches use the CSS Custom Highlight API: the DOM text is never rewritten. Reduced motion (OS or data-motion="reduce"): pills appear without rising, numbers change without a pop, scrolling is instant. Forced colors: the pill uses Canvas / CanvasText with a thicker border for errors, matches use Mark / MarkText.

Esta página se tradujo con IA. Informar de un problema de traducción