Exclusivebeta

Dex — <mv-dex>

Numbered collection grid where every entry has three levels of knowledge, so what is missing is as visible as what is done: unknown (an empty numbered slot with a dashed outline and an optional hint), seen (the artwork flattened into a one-tone silhouette, the name, details locked and a “how to get it” hint) and owned (full-color card with summary, description, facts and a call to action). Numbering gaps become nameless slots, so a user sees the holes in the series. Leveling up animates in place: the silhouette resolves out of a blur on discovery, then the full artwork fills in from the bottom on ownership, with a one-shot pulse (a plain crossfade under reduced motion). A summary header shows “12 of 30 used” with a segmented bar (owned / seen / unknown, told apart by shape as well as color) and filter pills with counts; a sticky detail panel follows the focused entry. The grid is a real listbox with 2D arrow navigation by visual row, Home/End, PageUp/PageDown and typeahead on names; mark(id, state) drives it and emits the cancelable mv-discover and mv-own, progress can persist to localStorage (knowledge never goes backward on reload), and state labels adapt to the domain (Used, Earned, Completed…).

CategoryData display
TypeWeb Component (<mv-dex>)
Statusbeta
Also installsbutton
Keywordsexclusive, culture, collection, discovery, feature-discovery, achievements, badges, gamification, progress, onboarding, curriculum, unlock, locked, silhouette, listbox, grid, keyboard, persist

When to use

Avoid when

Install

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

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["dex"], 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/focus.js, components/dex/dex.js, components/dex/dex.css, components/button/button.css.

Usage

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

<div id="dx-demo" style="width:min(100%,68rem);margin-inline:auto">
  <style>
    #dx-demo { display:grid; gap:1.25rem; align-content:start }
    #dx-demo .dx-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 }
    #dx-demo .dx-top { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1rem 1.25rem; border-bottom:1px solid var(--mv-border) }
    #dx-demo .dx-top h3 { display:flex; align-items:center; gap:.5rem; margin:0; font-size:1rem; letter-spacing:-.01em }
    #dx-demo .dx-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #dx-demo .dx-inner { padding:1.25rem }
    #dx-demo .dx-controls { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.875rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #dx-demo .dx-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #dx-demo .dx-k { color:var(--mv-fg-muted); font-size:.75rem; font-weight:600 }
    #dx-demo .dx-log { flex:1 1 16rem; min-height:1.25rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
    #dx-demo .dx-choice { font-size:.8125rem }
  </style>

  <!-- Icon sprite: symbols inherit stroke styling from the component -->
  <svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
    <symbol id="dx-i-dash" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="8" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="5" rx="1.5"/><rect x="13.5" y="11.5" width="7" height="9" rx="1.5"/><rect x="3.5" y="14.5" width="7" height="6" rx="1.5"/></symbol>
    <symbol id="dx-i-pulse" viewBox="0 0 24 24"><path d="M3 12h4l2.5-6.5 5 13L17 12h4"/></symbol>
    <symbol id="dx-i-funnel" viewBox="0 0 24 24"><path d="M3.5 4.5h17l-6.5 8v6l-4 2v-8z"/></symbol>
    <symbol id="dx-i-retention" viewBox="0 0 24 24"><path d="M4 4v16h16"/><path d="M7.5 8.5c2 5 5.5 7.5 11 8"/></symbol>
    <symbol id="dx-i-cohort" viewBox="0 0 24 24"><circle cx="9" cy="8.5" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 5a3.5 3.5 0 0 1 0 7"/><path d="M18.5 14.5A6.5 6.5 0 0 1 21.5 20"/></symbol>
    <symbol id="dx-i-replay" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="M10 9.25v5.5L14.75 12z"/></symbol>
    <symbol id="dx-i-bell" viewBox="0 0 24 24"><path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 2h-15z"/><path d="M10 21h4"/></symbol>
    <symbol id="dx-i-calendar" viewBox="0 0 24 24"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/></symbol>
    <symbol id="dx-i-flame" viewBox="0 0 24 24"><path d="M12 3c.8 3.2 5 5.4 5 10a5 5 0 0 1-10 0c0-2.2 1-3.6 2.2-4.6.2 1.8 1 2.8 2.3 3.1C11 8.8 10.8 6 12 3z"/></symbol>
    <symbol id="dx-i-flag" viewBox="0 0 24 24"><path d="M5.5 21V4"/><path d="M5.5 4.5h12l-2.5 4 2.5 4h-12"/></symbol>
    <symbol id="dx-i-beaker" viewBox="0 0 24 24"><path d="M9 3.5h6M10 3.5v6l-5.2 8.6A2 2 0 0 0 6.5 21h11a2 2 0 0 0 1.7-2.9L14 9.5v-6"/><path d="M7.5 15h9"/></symbol>
    <symbol id="dx-i-db" viewBox="0 0 24 24"><ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5v13c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5v-13"/><path d="M5 12c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/></symbol>
    <symbol id="dx-i-link" viewBox="0 0 24 24"><path d="M9.5 17H7a5 5 0 0 1 0-10h2.5"/><path d="M14.5 7H17a5 5 0 0 1 0 10h-2.5"/><path d="M8 12h8"/></symbol>
    <symbol id="dx-i-sync" viewBox="0 0 24 24"><path d="M19.5 10.5A7.5 7.5 0 0 0 6 6.8L4.5 8.5"/><path d="M4.5 4.5v4h4"/><path d="M4.5 13.5A7.5 7.5 0 0 0 18 17.2l1.5-1.7"/><path d="M19.5 19.5v-4h-4"/></symbol>
    <symbol id="dx-i-note" viewBox="0 0 24 24"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-5 4z"/></symbol>
    <symbol id="dx-i-share" viewBox="0 0 24 24"><circle cx="17.5" cy="5.5" r="2.5"/><circle cx="6.5" cy="12" r="2.5"/><circle cx="17.5" cy="18.5" r="2.5"/><path d="m8.7 10.8 6.6-4M8.7 13.2l6.6 4"/></symbol>
    <symbol id="dx-i-shield" viewBox="0 0 24 24"><path d="M12 3 5 6v5.5c0 4.3 3 7.9 7 9.5 4-1.6 7-5.2 7-9.5V6z"/></symbol>
    <symbol id="dx-i-key" viewBox="0 0 24 24"><circle cx="8" cy="15.5" r="4"/><path d="m11 12.5 8.5-8.5M16.5 7l2.5 2.5"/></symbol>
    <symbol id="dx-i-file" viewBox="0 0 24 24"><path d="M14 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8z"/><path d="M14 3.5V8h4.5M9 13h6M9 16.5h4"/></symbol>
    <symbol id="dx-i-code" viewBox="0 0 24 24"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></symbol>
    <symbol id="dx-i-phone" viewBox="0 0 24 24"><rect x="7" y="2.5" width="10" height="19" rx="2.5"/><path d="M11 18.5h2"/></symbol>
    <symbol id="dx-i-spark" viewBox="0 0 24 24"><path d="m11 3 1.9 5.1L18 10l-5.1 1.9L11 17l-1.9-5.1L4 10l5.1-1.9z"/><path d="m18.5 15 .8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8z"/></symbol>
  </svg>

  <!-- Main: feature discovery in a product analytics app -->
  <section class="dx-card" aria-label="Feature discovery">
    <div class="dx-top">
      <div>
        <h3>Your team in Lumen <span class="mv-badge" data-variant="info" data-shape="pill">Northwind Labs</span></h3>
        <p>What your workspace uses, what it has seen and what is still waiting to be found.</p>
      </div>
      <label class="mv-choice dx-choice" data-control="end">
        <input type="checkbox" role="switch" class="mv-switch" id="dx-panel" checked>
        <span class="mv-choice-text"><span class="mv-choice-title">Details panel</span></span>
      </label>
    </div>
    <div class="dx-inner">
      <mv-dex id="dx-main" label="Lumen features" owned-label="Used" total="24" selected="webhooks">
        <div data-entry="dashboards" data-number="1" data-state="owned" data-name="Dashboards" data-icon="#dx-i-dash" data-summary="42 dashboards · used today" data-description="Pin charts, tables and notes to shared boards that refresh every five minutes." data-href="#dashboards"></div>
        <div data-entry="events" data-number="2" data-state="owned" data-name="Event tracking" data-icon="#dx-i-pulse" data-summary="3.2M events this month" data-href="#events"></div>
        <div data-entry="funnels" data-number="3" data-state="owned" data-name="Funnels" data-icon="#dx-i-funnel" data-summary="Used 2 days ago" data-href="#funnels"></div>
        <div data-entry="retention" data-number="4" data-state="owned" data-name="Retention" data-icon="#dx-i-retention" data-summary="Used last week" data-href="#retention"></div>
        <div data-entry="cohorts" data-number="5" data-state="seen" data-name="Cohorts" data-icon="#dx-i-cohort" data-hint="Save a segment from any chart to turn it into a cohort." data-href="#cohorts"></div>
        <div data-entry="session-replay" data-number="6" data-state="seen" data-name="Session replay" data-icon="#dx-i-replay" data-hint="Install the web SDK v4 and turn on recording." data-href="#replay" data-cta="Set up recording" data-summary="First replay watched today" data-description="Watch real sessions with clicks, scrolls and rage-clicks highlighted, linked to every funnel step."></div>
        <div data-entry="alerts" data-number="7" data-state="owned" data-name="Alerts" data-icon="#dx-i-bell" data-summary="6 alerts · last fired Sep 21" data-href="#alerts"></div>
        <div data-entry="reports" data-number="8" data-state="owned" data-name="Scheduled reports" data-icon="#dx-i-calendar" data-summary="Weekly to 14 people" data-href="#reports"></div>
        <div data-entry="heatmaps" data-number="9" data-state="unknown" data-name="Heatmaps" data-icon="#dx-i-flame" data-hint="Hidden in the page menu of any chart." data-href="#heatmaps" data-cta="Open a heatmap"></div>
        <div data-entry="flags" data-number="10" data-state="seen" data-name="Feature flags" data-icon="#dx-i-flag" data-hint="Create a flag from Settings, then Experiments." data-href="#flags"></div>
        <div data-entry="ab-tests" data-number="12" data-state="unknown" data-name="A/B tests" data-icon="#dx-i-beaker" data-hint="Unlocks after your first feature flag."></div>
        <div data-entry="sql" data-number="13" data-state="owned" data-name="SQL editor" data-icon="#dx-i-db" data-summary="118 saved queries" data-href="#sql">
          <dl data-details class="mv-dex-facts">
            <div><dt>Queries this month</dt><dd>1,284</dd></div>
            <div><dt>Median run time</dt><dd>1.8 s</dd></div>
          </dl>
        </div>
        <div data-entry="webhooks" data-number="14" data-state="seen" data-name="Webhooks" data-icon="#dx-i-link" data-hint="Add an endpoint in Settings, then Developers, and send a test event." data-href="#webhooks" data-cta="Set up webhooks" data-summary="1 endpoint · 200 OK" data-description="Stream events to your own servers in real time, signed and retried for 72 hours."></div>
        <div data-entry="warehouse" data-number="15" data-state="unknown" data-name="Warehouse sync" data-icon="#dx-i-sync" data-hint="Available on the Scale plan."></div>
        <div data-entry="annotations" data-number="16" data-state="owned" data-name="Annotations" data-icon="#dx-i-note" data-summary="Used yesterday" data-href="#annotations"></div>
        <div data-entry="shared-links" data-number="17" data-state="owned" data-name="Shared links" data-icon="#dx-i-share" data-summary="9 public links" data-href="#links"></div>
        <div data-entry="roles" data-number="18" data-state="seen" data-name="Roles &amp; permissions" data-icon="#dx-i-shield" data-hint="Invite a teammate as Viewer to try custom roles." data-href="#roles"></div>
        <div data-entry="sso" data-number="19" data-state="unknown" data-name="Single sign-on" data-icon="#dx-i-key" data-hint="Available on the Enterprise plan."></div>
        <div data-entry="audit-log" data-number="20" data-state="unknown" data-name="Audit log" data-icon="#dx-i-file"></div>
        <div data-entry="api-keys" data-number="21" data-state="owned" data-name="API keys" data-icon="#dx-i-code" data-summary="3 active keys" data-href="#keys"></div>
        <div data-entry="mobile-sdk" data-number="23" data-state="seen" data-name="Mobile SDK" data-icon="#dx-i-phone" data-hint="Add the iOS or Android package and send one event." data-href="#mobile"></div>
        <div data-entry="ai-insights" data-number="24" data-state="unknown" data-name="AI insights" data-icon="#dx-i-spark" data-hint="Rolling out this fall."></div>
      </mv-dex>
    </div>
    <div class="dx-controls">
      <span class="dx-actions">
        <span class="dx-k">Simulate:</span>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dx-discover">Open a heatmap</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dx-webhook">Send a test webhook</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dx-replay">Watch a replay</button>
        <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="dx-reset">Reset</button>
      </span>
      <p class="dx-log" id="dx-log" aria-live="polite">Arrow keys move through the grid · type a name to jump</p>
    </div>
  </section>

  <!-- Compact: a learning path without the side panel -->
  <section class="dx-card" aria-label="Learning path">
    <div class="dx-top">
      <div>
        <h3>SQL for analysts</h3>
        <p>Lessons unlock as you go; locked ones say what opens them.</p>
      </div>
    </div>
    <div class="dx-inner">
      <mv-dex id="dx-path" label="Learning path" owned-label="Completed" seen-label="Unlocked" unknown-label="Locked" density="compact" detail="none" filters="false" pad="2">
        <div data-entry="select" data-state="owned" data-name="SELECT basics" data-icon="#dx-i-db" data-summary="12 min"></div>
        <div data-entry="where" data-state="owned" data-name="Filtering rows" data-icon="#dx-i-funnel" data-summary="15 min"></div>
        <div data-entry="order" data-state="owned" data-name="Sorting" data-icon="#dx-i-retention" data-summary="8 min"></div>
        <div data-entry="group" data-state="owned" data-name="Aggregates" data-icon="#dx-i-dash" data-summary="20 min"></div>
        <div data-entry="joins" data-state="seen" data-name="Joins" data-icon="#dx-i-link" data-hint="Next up · 25 min"></div>
        <div data-entry="subqueries" data-state="seen" data-name="Subqueries" data-icon="#dx-i-code" data-hint="18 min"></div>
        <div data-entry="dates" data-state="unknown" data-name="Dates &amp; time" data-icon="#dx-i-calendar" data-hint="After Joins"></div>
        <div data-entry="windows" data-state="unknown" data-name="Window functions" data-icon="#dx-i-pulse" data-hint="After Subqueries"></div>
        <div data-entry="ctes" data-state="unknown" data-name="CTEs" data-icon="#dx-i-file"></div>
        <div data-entry="perf" data-state="unknown" data-name="Query performance" data-icon="#dx-i-flame"></div>
        <div data-entry="capstone" data-state="unknown" data-name="Capstone project" data-icon="#dx-i-spark" data-hint="Final lesson"></div>
      </mv-dex>
    </div>
  </section>

  <script type="module">
    const dex = document.getElementById("dx-main");
    await customElements.whenDefined("mv-dex");
    const $ = (id) => document.getElementById(id);
    const log = $("dx-log");
    const initial = dex.state;
    const say = (text) => { log.textContent = text; };

    const sync = () => {
      const s = dex.state;
      $("dx-discover").disabled = s.heatmaps !== "unknown";
      $("dx-webhook").disabled = s.webhooks === "owned";
      $("dx-replay").disabled = s["session-replay"] === "owned";
    };
    dex.addEventListener("mv-discover", (e) => say(`mv-discover · No. ${e.detail.number} ${e.detail.name}`));
    dex.addEventListener("mv-own", (e) => say(`mv-own · No. ${e.detail.number} ${e.detail.name} (from ${e.detail.from})`));
    dex.addEventListener("mv-select", (e) => say(`mv-select · No. ${e.detail.number} (${e.detail.state})`));
    dex.addEventListener("mv-change", sync);
    dex.addEventListener("click", (e) => { if (e.target.closest("a[href^='#']")) e.preventDefault(); });

    $("dx-discover").addEventListener("click", () => { dex.select("heatmaps"); dex.mark("heatmaps", "seen", { source: "demo" }); });
    $("dx-webhook").addEventListener("click", () => { dex.select("webhooks"); dex.mark("webhooks", "owned", { source: "demo" }); });
    $("dx-replay").addEventListener("click", () => { dex.select("session-replay"); dex.mark("session-replay", "owned", { source: "demo" }); });
    $("dx-reset").addEventListener("click", () => { dex.state = initial; dex.filter = "all"; dex.select("webhooks"); sync(); say("Progress reset."); });
    $("dx-panel").addEventListener("change", (e) => { dex.detail = e.target.checked ? "side" : "none"; });
    sync();
  </script>
</div>

Cultural reference

Pokémon Red and Green — Game Freak / Nintendo (1996, game). The game's encyclopedia lists every creature by number in three states, unknown (just a number), seen (a glimpse and a name, no data) and caught (the full entry), and the gaps in the numbered list are what drive players to complete it. In the UI, any collection (features used, badges earned, lessons completed) becomes a numbered grid of unknown slots, silhouettes with a hint on how to get them and full cards, with gaps left visible and each level-up revealed in place.

API

Attributes

NameTypeDefaultDescription
labelstringCollectionName of the collection: shown above the score and used as the accessible name of the grid and the progress bar.
owned-label / seen-label / unknown-labelstringOwned / Seen / MissingDomain words for the three states (e.g. Used / Seen / Missing for features, Completed / Unlocked / Locked for lessons, Earned for badges). Used in the filters, the score (“12 of 30 used”), the detail panel and every accessible name (“seen, not yet used”). Past participles read best.
filterall | owned | seen | unknownallCurrent filter (reflected; the filter pills set it). An entry that changes state under a filter stays visible until the filter changes, so nothing jumps away mid-interaction.
selectedstring (entry id)Active entry shown in the detail panel (reflected as the user moves). Defaults to the first entry.
totalnumberSize of the series. Missing numbers up to it (and any gap between numbered entries) are rendered as nameless unknown slots, so secret entries never reach the DOM.
padnumber3Digits of the displayed number (014).
detailside | below | nonesideDetail panel beside the grid (stacks below it when the element is narrower than 40rem), always below, or hidden.
densitycompactSmaller tiles and artwork, for long series or narrow columns.
silhouetteshape | blurshapeHow seen images are hidden: shape uses the image alpha as a one-tone mask (icons, transparent PNG/SVG); blur shows a grayscale, blurred, low-contrast version (photos). SVG icons are always flattened into a solid shape.
filters"true" | "false"true"false" hides the filter pills (score and bar stay).
persiststringlocalStorage key: progress is saved on every change and restored on load. Stored states only raise what the markup says (knowledge never goes backward); reset() clears it.
data-entry (on children)string (id)Declares an entry in markup. Other fields: data-number, data-state (unknown | seen | owned), data-name, data-hint, data-summary, data-description, data-href, data-cta (link label while not owned; owned entries link as “Open {name}”), data-icon (#sprite-id or image URL), data-image. A child <svg> or <img> is the artwork; a child [data-details] is rich owned-only content moved into the panel.

Properties

NameTypeDescription
entriesArray<{ id, number?, name, state?, hint?, summary?, description?, href?, cta?, icon?: Element | "#sprite-id" | url, image?, details?: Element | Array<{ label, value }> }>Entries as data (replaces markup). Unnumbered entries take the lowest free numbers; an entry without a name can only be unknown. Can be set before the element is defined.
stateRecord<id, "unknown" | "seen" | "owned">Knowledge map. Setting it (e.g. from your server) applies silently: no events, no animation.
counts{ owned, seen, unknown, known, total }Read-only counts; known = owned + seen.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (keys: label, all, owned, seen, unknown, number, score, percent, progress, filters, details, nameOwned, nameSeen, nameUnknown, unknownTitle, unknownText, locked, howTo, open ({name}), learn, empty, announceDiscover, announceOwn, announceLost, announceFilter). English defaults.

Methods

NameDescription
mark(id, state, { animate?, announce?, source? })Moves an entry (id, or number) to a state. Going up emits the cancelable mv-discover or mv-own first; going down is silent except for mv-change. Returns true when applied, false if vetoed, unknown id or a nameless slot.
discover(id) / own(id)Shorthands for mark(id, "seen") and mark(id, "owned").
select(id, { focus? })Makes an entry active in the panel (clears a filter that hides it).
reset()Clears persisted progress and returns every entry to its initial state, silently.

Events

NameDescription
mv-discoverCancelable, before an entry goes from unknown to seen. detail: { id, number, name, from, to, source, entry }. preventDefault() keeps it hidden (e.g. until your server confirms).
mv-ownCancelable, before an entry becomes owned (from seen, or straight from unknown with from: "unknown"). Same detail as mv-discover.
mv-changeAfter any state change, up or down. detail: { id, from, to, source, counts }. Sync your backend here.
mv-selectThe user moved to another entry (click, arrows, typeahead, Tab). detail: { id, number, state, entry }.
mv-activateEnter, Space, double click or the panel's call to action on an entry. detail: { id, number, state, source: "keyboard" | "pointer" | "link", entry }.

Content structure

NameDescription
[data-entry] childrenEntries declared in markup (read once, then replaced by the rendered grid). Their [data-details] child is moved, not cloned, so its listeners survive.

CSS classes

NameDescription
mv-dex-headSummary: -title, -scoreline (-score with -count, -pct), -bar (role=progressbar, .mv-dex-seg[data-state]), -filters (.mv-dex-filter[aria-pressed] with -swatch, -filter-label, -filter-count).
mv-dex-grid / mv-dex-entryThe listbox and its options; data-state="unknown | seen | owned", data-flash="discover | own" for a moment after leveling up. Parts: -num, -mark, -art, -name, -note.
mv-dex-artArtwork box with data-state and three stacked layers: -ghost (big number for unknown), -sil (silhouette), -full (full artwork, revealed by clip-path).
mv-dex-panelDetail panel (data-state): -panel-head, -panel-num, -panel-name, -status, -panel-body (-lead, -text, -facts, -locked, -howto, -cta).

CSS variables

NameDefaultDescription
--mv-dex-ownedvar(--mv-accent)Owned color: bar segment, swatch, check mark, selection ring.
--mv-dex-owned-bg / --mv-dex-owned-inkaccent tint / var(--mv-accent-fg)Background and icon color of owned artwork.
--mv-dex-seenaccent mixed with --mv-bg-emphasisSeen color (hatched bar segment, half swatch, discovery pulse).
--mv-dex-silhouettemix of --mv-fg and --mv-bg-mutedTone of silhouettes.
--mv-dex-min8.5rem (compact 6.75rem)Minimum tile width (auto-fill columns).
--mv-dex-art-size3.25rem (compact 2.5rem)Artwork size in tiles.
--mv-dex-panel-width18remWidth of the side panel.
--mv-dex-gapvar(--mv-space-2)Gap between tiles.

Accessibility

The grid is a real single-select listbox (aria-labelledby the collection title) whose options use a roving tabindex: Tab enters on the active entry, arrows move left/right and up/down by visual row (columns are measured, RTL mirrors left and right), Home/End jump to the ends, PageUp/PageDown move three rows, typing letters jumps to a known entry by name, Enter/Space/double click emit mv-activate; selection follows focus and drives the detail panel. Each option's accessible name carries its number, name and state in words (“No. 14, Webhooks, seen, not yet used”; unknown entries say “No. 21, not discovered yet” and never expose their name), and its hint or summary is linked with aria-describedby. State is never conveyed by the silhouette or color alone: tiles differ by border style (dashed slot), mark icon (check / eye) and text, swatches and bar segments differ by shape (full, half, hollow; hatched segment), and the panel shows a status pill with icon and label. The progress bar is a role=progressbar with an aria-valuetext of all three counts; filter pills are toggle buttons (aria-pressed) with their count in their name. Level-ups, filter changes and downgrades are announced once in a polite live region (“Discovered Heatmaps, number 9. 17 of 24 discovered.”, “Webhooks is now used. 11 of 24 used.”); focus is never moved by state changes, and an entry that changes state under a filter stays in place until the filter changes. Reduced motion (OS or data-motion="reduce"): the blur-and-fill reveal becomes a 200 ms crossfade and the pulses are dropped. Forced colors: selection uses Highlight, silhouettes GrayText, and the bar and swatches keep system-colored shapes.