Exclusivebeta

Fog — <mv-fog>

Progressive discovery for dense interfaces (admin consoles, pro tools, analytics apps with many panels, deep settings): every area marked data-fog that the user has not explored yet sits under a light, textured veil (a haze of the page color, soft cloud patches, a slight desaturation and a fine grain, all CSS, no canvas) with a tiny “Not explored yet · Automations” tag, while its content stays readable, clickable and fully exposed to assistive technology. Resting the pointer on an area thins the fog and fills a small ring over dwell ms, then the veil lifts in a clearing that opens from the exact point of interaction; keyboard focus, a press inside, an optional scroll-into-view rule or explore(id) from your code lift it too. Explored areas never fog over again: progress persists with storage-key (synced across tabs) or comes from your server through the explored property. A meter (“5 of 12 areas explored” beside a tiny map with one tile per area) opens a list of what is left, with hints, that scrolls to and focuses an area; an optional “Reveal all” serves experts and reset() brings the fog back. mv-explore is cancelable (veto dwell-based discovery, count only real use), mv-fog-jump lets you open the tab that holds a hidden area, mv-fog-cleared fires once everything is known. intensity scales the veil, and nothing ever turns red or blocks a click.

CategoryLayout
TypeWeb Component (<mv-fog>)
Statusbeta
Also installsbutton
Keywordsexclusive, culture, progressive-disclosure, discovery, onboarding, feature-discovery, exploration, fog-of-war, adoption, admin, dashboard, veil, progress, coach, persistence

When to use

Avoid when

Install

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

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

Usage

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

<div id="fg-demo" style="width:min(100%,70rem);margin-inline:auto">
  <style>
    #fg-demo { display:grid; gap:1rem; font-size:.8125rem }
    #fg-demo .fg-app { display:grid; grid-template-columns:15rem minmax(0,1fr); border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #fg-demo .fg-side { display:flex; flex-direction:column; gap:1rem; padding:1rem .75rem .75rem; border-inline-end:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #fg-demo .fg-brand { display:flex; align-items:center; gap:.5rem; padding:0 .375rem; font-weight:650; font-size:.9375rem; letter-spacing:-.01em }
    #fg-demo .fg-logo { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:var(--mv-radius-md); background:var(--mv-accent); color:var(--mv-fg-on-accent) }
    #fg-demo .fg-logo svg, #fg-demo .fg-nav svg { width:1rem; height:1rem }
    #fg-demo .fg-nav { display:grid; gap:1px; margin:0; padding:0; list-style:none }
    #fg-demo .fg-nav a { display:flex; align-items:center; gap:.625rem; padding:.4375rem .5rem; border-radius:var(--mv-radius-md); color:var(--mv-fg-muted); font-weight:500; text-decoration:none }
    #fg-demo .fg-nav a:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
    #fg-demo .fg-nav a[aria-current] { background:var(--mv-surface); color:var(--mv-fg); box-shadow:var(--mv-shadow-xs) }
    #fg-demo .fg-block { display:grid; gap:.25rem; padding:.625rem .5rem; border-radius:var(--mv-radius-lg) }
    #fg-demo .fg-k { margin:0 0 .125rem; padding:0 .125rem; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase }
    #fg-demo .fg-views a { display:flex; align-items:center; justify-content:space-between; padding:.3125rem .375rem; border-radius:var(--mv-radius-sm); color:var(--mv-fg-muted); text-decoration:none }
    #fg-demo .fg-views a:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
    #fg-demo .fg-views span { color:var(--mv-fg-subtle); font-variant-numeric:tabular-nums; font-size:.75rem }
    #fg-demo .fg-team { display:flex; align-items:center; gap:.5rem; padding:0 .125rem }
    #fg-demo .fg-faces { display:flex }
    #fg-demo .fg-face { display:grid; place-items:center; width:1.625rem; height:1.625rem; margin-inline-start:-.375rem; border:2px solid var(--mv-bg-subtle); border-radius:50%; background:var(--mv-bg-emphasis); font-size:.625rem; font-weight:600 }
    #fg-demo .fg-face:first-child { margin-inline-start:0 }
    #fg-demo .fg-team small { color:var(--mv-fg-muted); font-size:.75rem }
    #fg-demo .fg-meter { margin-top:auto }
    #fg-demo .fg-main { display:grid; align-content:start; gap:1rem; min-width:0; padding:0 1.125rem 1.125rem }
    #fg-demo .fg-top { display:flex; align-items:center; gap:.75rem; height:3.5rem; margin:0 -1.125rem; padding:0 1.125rem; border-bottom:1px solid var(--mv-border) }
    #fg-demo .fg-search { display:flex; align-items:center; gap:.5rem; width:min(100%,24rem); height:2.125rem; padding:0 .375rem 0 .625rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-surface); color:var(--mv-fg-subtle); font:inherit; cursor:text }
    #fg-demo .fg-search svg { width:.9375rem; height:.9375rem; flex:none }
    #fg-demo .fg-search span:first-of-type { flex:1; text-align:start }
    #fg-demo .fg-me { display:grid; place-items:center; width:2rem; height:2rem; margin-inline-start:auto; border-radius:50%; background:var(--mv-bg-emphasis); font-size:.75rem; font-weight:600 }
    #fg-demo .fg-head { display:flex; align-items:end; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap }
    #fg-demo .fg-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.015em }
    #fg-demo .fg-head p { margin:.125rem 0 0; color:var(--mv-fg-muted) }
    #fg-demo .fg-filters { display:flex; align-items:center; gap:.375rem; padding:.25rem; border-radius:var(--mv-radius-lg) }
    #fg-demo .fg-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.875rem }
    #fg-demo .fg-card { display:grid; align-content:start; gap:.75rem; min-width:0; padding:.875rem 1rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
    #fg-demo .fg-card[data-span="2"] { grid-column:span 2 }
    #fg-demo .fg-card header { display:flex; align-items:center; justify-content:space-between; gap:.5rem; min-height:1.375rem }
    #fg-demo .fg-card h4 { margin:0; font-size:.8125rem; font-weight:600 }
    #fg-demo .fg-card header small { color:var(--mv-fg-subtle); font-size:.75rem }
    #fg-demo .fg-kpis { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.75rem }
    #fg-demo .fg-kpi { display:grid; gap:.125rem }
    #fg-demo .fg-kpi span { color:var(--mv-fg-muted); font-size:.75rem }
    #fg-demo .fg-kpi b { font-size:1.375rem; font-weight:650; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
    #fg-demo .fg-kpi em { color:var(--mv-success); font-style:normal; font-size:.75rem; font-weight:600 }
    #fg-demo .fg-spark { width:100%; height:2.5rem; color:var(--mv-accent) }
    #fg-demo .fg-rows { display:grid; gap:.375rem; margin:0; padding:0; list-style:none }
    #fg-demo .fg-rows li { display:flex; align-items:center; justify-content:space-between; gap:.5rem; min-width:0; font-size:.78rem }
    #fg-demo .fg-rows li > span:first-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #fg-demo .fg-rows .fg-num { color:var(--mv-fg-muted); font-variant-numeric:tabular-nums; white-space:nowrap }
    #fg-demo .fg-mono { font-family:var(--mv-font-mono); font-size:.72rem; color:var(--mv-fg-muted) }
    #fg-demo .fg-sub { display:block; color:var(--mv-fg-subtle); font-size:.7rem }
    #fg-demo .fg-card .mv-choice { justify-content:space-between; font-size:.78rem }
    #fg-demo .fg-foot { display:flex; gap:.5rem }
    #fg-demo .fg-dot { display:inline-block; width:.4375rem; height:.4375rem; margin-inline-end:.375rem; border-radius:50%; background:var(--mv-success); vertical-align:.05em }
    #fg-demo .fg-controls { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.875rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
    #fg-demo .fg-ctl { display:flex; align-items:center; gap:.625rem; flex-wrap:wrap }
    #fg-demo .fg-ctl > span { color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
    #fg-demo .fg-log { flex:1 1 100%; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    @media (max-width:60rem) {
      #fg-demo .fg-app { grid-template-columns:minmax(0,1fr) }
      #fg-demo .fg-side { border-inline-end:0; border-bottom:1px solid var(--mv-border) }
      #fg-demo .fg-nav { display:none }
      #fg-demo .fg-grid { grid-template-columns:repeat(2,minmax(0,1fr)) }
    }
    @media (max-width:36rem) {
      #fg-demo .fg-grid { grid-template-columns:minmax(0,1fr) }
      #fg-demo .fg-card[data-span="2"] { grid-column:auto }
    }
  </style>

  <!-- An operations console with 12 areas: 5 already explored, 7 still under the fog -->
  <mv-fog id="fg-console" reveal-all placement="right-end" explored="overview,orders,inventory,filters,views">
    <div class="fg-app">
      <aside class="fg-side" aria-label="Harborline navigation">
        <span class="fg-brand">
          <span class="fg-logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 17c2 1.5 4 1.5 6 0s4-1.5 6 0 4 1.5 6 0"/><path d="M12 3v10M8 7l4-4 4 4"/></svg></span>
          Harborline
        </span>
        <ul class="fg-nav">
          <li><a href="#" aria-current="page"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>Operations</a></li>
          <li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18M16 10a4 4 0 0 1-8 0"/></svg>Orders</a></li>
          <li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21 8-9-5-9 5 9 5z"/><path d="m3 8v8l9 5 9-5V8M12 13v8"/></svg>Products</a></li>
          <li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0M16 4.5a3.5 3.5 0 0 1 0 7M18.5 14.5a6.5 6.5 0 0 1 3 5.5"/></svg>Customers</a></li>
          <li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>Analytics</a></li>
        </ul>
        <section class="fg-block fg-views" data-fog="views" data-fog-label="Saved views" data-fog-hint="Pin the filters you use every day" aria-label="Saved views">
          <p class="fg-k">Saved views</p>
          <a href="#">Late shipments <span>14</span></a>
          <a href="#">VIP customers <span>212</span></a>
          <a href="#">Refund requests <span>6</span></a>
        </section>
        <section class="fg-block" data-fog="team" data-fog-label="Team" data-fog-hint="See who is online and hand off orders" aria-label="Team">
          <p class="fg-k">Team</p>
          <div class="fg-team">
            <span class="fg-faces" aria-hidden="true"><span class="fg-face">AS</span><span class="fg-face">KW</span><span class="fg-face">PR</span><span class="fg-face">LF</span></span>
            <small>4 teammates online</small>
          </div>
        </section>
        <div class="fg-meter" data-fog-meter></div>
      </aside>

      <div class="fg-main">
        <header class="fg-top">
          <div data-fog="commands" data-fog-label="Command bar" data-fog-hint="Search anything or run an action with ⌘K" style="border-radius:var(--mv-radius-md)">
            <button type="button" class="fg-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>Search orders, customers, or run a command</span>
              <kbd class="mv-kbd" data-size="sm">⌘K</kbd>
            </button>
          </div>
          <span class="fg-me" aria-hidden="true">MR</span>
        </header>

        <div class="fg-head">
          <div>
            <h3>Operations</h3>
            <p>Thursday, September 24, 2026 · North America store</p>
          </div>
          <div class="fg-filters" data-fog="filters" data-fog-label="Filters" aria-label="Filters">
            <button type="button" class="mv-button" data-variant="secondary" data-size="sm">Today</button>
            <button type="button" class="mv-button" data-variant="ghost" data-size="sm">7 days</button>
            <button type="button" class="mv-button" data-variant="ghost" data-size="sm">30 days</button>
          </div>
        </div>

        <div class="fg-grid">
          <section class="fg-card" data-span="2" data-fog="overview" data-fog-label="Overview" aria-label="Overview">
            <header><h4>Overview</h4><small>Updated 2 min ago</small></header>
            <div class="fg-kpis">
              <div class="fg-kpi"><span>Revenue</span><b>$48,210</b><em>+8.4%</em></div>
              <div class="fg-kpi"><span>Orders</span><b>1,284</b><em>+3.1%</em></div>
              <div class="fg-kpi"><span>Conversion</span><b>3.2%</b><em>+0.4 pt</em></div>
            </div>
            <svg class="fg-spark" viewBox="0 0 300 40" preserveAspectRatio="none" aria-hidden="true"><path d="M0 32 L25 28 L50 30 L75 22 L100 25 L125 18 L150 20 L175 13 L200 16 L225 10 L250 12 L275 6 L300 8" fill="none" stroke="currentColor" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>
          </section>

          <section class="fg-card" data-fog="inventory" data-fog-label="Inventory" aria-label="Inventory">
            <header><h4>Low stock</h4><span class="mv-badge" data-variant="warning">3 items</span></header>
            <ul class="fg-rows">
              <li><span>Linen tote, natural</span><span class="fg-num">8 left</span></li>
              <li><span>Stoneware mug, 12 oz</span><span class="fg-num">14 left</span></li>
              <li><span>Canvas apron, olive</span><span class="fg-num">5 left</span></li>
            </ul>
          </section>

          <section class="fg-card" data-fog="orders" data-fog-label="Orders" aria-label="Recent orders">
            <header><h4>Recent orders</h4><small>Live</small></header>
            <ul class="fg-rows">
              <li><span>#10482 · Lena Fischer</span><span class="fg-num">$184.00</span></li>
              <li><span>#10481 · Omar Haddad</span><span class="fg-num">$62.50</span></li>
              <li><span>#10480 · Sofia Rossi</span><span class="fg-num">$329.90</span></li>
            </ul>
          </section>

          <section class="fg-card" data-fog="automations" data-fog-label="Automations" data-fog-hint="Rules that tag, restock and notify for you" aria-label="Automations">
            <header><h4>Automations</h4><small>2 of 3 on</small></header>
            <label class="mv-choice" data-control="end"><span>Tag VIP customers</span><input type="checkbox" role="switch" class="mv-switch" data-size="sm" checked></label>
            <label class="mv-choice" data-control="end"><span>Reorder when stock &lt; 10</span><input type="checkbox" role="switch" class="mv-switch" data-size="sm" checked></label>
            <label class="mv-choice" data-control="end"><span>Pause ads on sold-out items</span><input type="checkbox" role="switch" class="mv-switch" data-size="sm"></label>
          </section>

          <section class="fg-card" data-fog="alerts" data-fog-label="Alerts" data-fog-hint="Get paged when refunds or payment failures spike" aria-label="Alerts">
            <header><h4>Alerts</h4><span class="mv-badge" data-variant="secondary">2 rules</span></header>
            <ul class="fg-rows">
              <li><span>Refund rate above 4%<span class="fg-sub">Email and Slack</span></span></li>
              <li><span>Payment failures ×3 in 10 min<span class="fg-sub">SMS to on-call</span></span></li>
            </ul>
          </section>

          <section class="fg-card" data-fog="webhooks" data-fog-label="Webhooks" data-fog-hint="Send order events to your ERP or warehouse" aria-label="Webhooks">
            <header><h4>Webhooks</h4><small><span class="fg-dot" aria-hidden="true"></span>Healthy</small></header>
            <ul class="fg-rows">
              <li><span class="fg-mono">order.created</span><span class="fg-num">200 · 84 ms</span></li>
              <li><span class="fg-mono">refund.issued</span><span class="fg-num">200 · 91 ms</span></li>
            </ul>
            <div class="fg-foot"><button type="button" class="mv-button" data-variant="outline" data-size="sm">Add endpoint</button></div>
          </section>

          <section class="fg-card" data-fog="reports" data-fog-label="Reports" data-fog-hint="Schedule PDF or CSV exports to your inbox" aria-label="Scheduled reports">
            <header><h4>Scheduled reports</h4><small>2 active</small></header>
            <ul class="fg-rows">
              <li><span>Weekly sales<span class="fg-sub">Mondays, 8:00 AM · PDF</span></span></li>
              <li><span>Sales tax summary<span class="fg-sub">Monthly · CSV</span></span></li>
            </ul>
          </section>

          <section class="fg-card" data-fog="audit" data-fog-label="Audit log" data-fog-hint="Every change, who made it and when" aria-label="Audit log">
            <header><h4>Audit log</h4><small>Today</small></header>
            <ul class="fg-rows">
              <li><span>Ana Souza changed shipping rates</span><span class="fg-num">2h</span></li>
              <li><span>Kenji Watanabe exported customers</span><span class="fg-num">4h</span></li>
              <li><span>Priya Raman added a webhook</span><span class="fg-num">6h</span></li>
            </ul>
          </section>
        </div>
      </div>
    </div>
  </mv-fog>

  <div class="fg-controls">
    <span class="fg-ctl">
      <span id="fg-int-label">Fog</span>
      <mv-segmented id="fg-intensity" aria-labelledby="fg-int-label" name="fog-intensity" value="0.5">
        <button value="0.3">Light</button>
        <button value="0.5">Balanced</button>
        <button value="0.8">Dense</button>
      </mv-segmented>
    </span>
    <span class="fg-ctl">
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fg-api">Explore Webhooks from code</button>
      <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="fg-reset">Reset exploration</button>
    </span>
    <p class="fg-log" id="fg-log">Rest the pointer on a veiled panel, Tab into it, or open the meter in the sidebar to jump to what is left.</p>
  </div>

  <script type="module">
    const fog = document.getElementById("fg-console");
    const log = document.getElementById("fg-log");
    fog.addEventListener("click", (e) => { if (e.target.closest("a[href='#']")) e.preventDefault(); });
    fog.addEventListener("mv-explore", (e) => {
      const { label, source, explored, total } = e.detail;
      log.textContent = `mv-explore · ${label} · via ${source} · ${explored} of ${total}`;
    });
    fog.addEventListener("mv-fog-cleared", (e) => { log.textContent = `mv-fog-cleared · all ${e.detail.total} areas explored`; });
    fog.addEventListener("mv-fog-reset", () => { log.textContent = "mv-fog-reset · the fog is back on every unexplored area"; });
    document.getElementById("fg-intensity").addEventListener("mv-change", (e) => { fog.intensity = Number(e.detail.value); });
    document.getElementById("fg-api").addEventListener("click", () => fog.explore("webhooks", { announce: true }));
    document.getElementById("fg-reset").addEventListener("click", () => {
      fog.reset();
    });
  </script>
</div>

Cultural reference

Sid Meier's Civilization — Sid Meier & Bruce Shelley (MicroProse) (1991, game). The map starts under a fog of war and only reveals itself around the places your units have explored, and explored land stays revealed for good. In the UI, areas the user has not explored yet sit under a light veil that lifts where they actually interact, stays lifted across sessions, and a meter shows how much of the interface is still unknown.

API

Attributes

NameTypeDefaultDescription
storage-keystringPersists explored ids in localStorage under mv-fog:<key> (opt-in) and syncs them across tabs through the storage event. Merged with the explored attribute on load; knowledge never goes backward until reset().
exploredcomma-separated idsAreas already explored when the page loads (e.g. rendered by your server). Setting it later replaces the explored set without events or animation.
intensitynumber (0..1)0.5Strength of the veil (wash, desaturation, blur and grain all scale with it). Sets --mv-fog-intensity on the element.
reveal-onspace-separated: pointer | focus | click | viewpointer focus clickWhat counts as exploring an area. pointer: the mouse or pen rests on the area for dwell ms (touch has no hover). focus: keyboard or programmatic focus enters it. click: a press inside it. view: at least 60% of the area stays on screen for 1.5 × dwell (long settings pages). Jumping from the list and explore() always work.
dwellnumber (ms)700How long the pointer must rest on an area before its fog lifts. The veil thins and the tag's ring fills over this time; leaving earlier restores it.
reveal-allbooleanfalseShows a “Reveal all” button in the list panel (experts, returning users).
meter"auto" | "none"autoauto renders the meter inside the first [data-fog-meter] descendant, or docks it as a sticky chip at the bottom end of the element. none hides it (build your own from progress, areas and the events).
tag-placementtop-end | top-start | bottom-end | bottom-start | center | nonetop-endCorner of each veil where the “Not explored yet” tag sits (most panels keep secondary metadata top end). none keeps the veil without a tag.
placementtop | bottom | left | right, with -start | -endtop-startPreferred side of the list panel relative to the meter button (flips and shifts to stay in the viewport).
data-fogstring (id)On any descendant box: marks an area. Several elements may share an id (a panel and its compact duplicate): exploring one reveals all of them. Areas can be added or removed at any time.
data-fog-label / data-fog-hintstringOn an area: name shown in the tag and the list (default: aria-label, then the id, prettified), and a one-line hint shown in the list under unexplored areas.
data-fog-stateveiled | clearing | revealing | exploredSet by the component on every area (styleable, e.g. a “new” dot on explored areas). data-fog-arrived is added for 1.4 s after a jump. data-cleared is set on mv-fog once every area is explored.
data-fog-meterempty elementPlaceholder where the meter is rendered (sidebar footer, toolbar…).

Properties

NameTypeDescription
exploredstring[]Explored ids. Assigning an array (from your server) replaces the set: newly known areas lose their veil instantly, forgotten ones get it back. Persisted when storage-key is set. No events.
areasArray<{ id, label, hint, explored, elements }>Areas currently in the DOM, in document order (read-only).
progress{ explored, total, remaining, ratio }Counts over the areas currently in the DOM (read-only). Explored ids of areas that are not rendered are kept but not counted.
stringsPartial<Record<string, string>>Overrides every visible text and announcement (keys: unexplored, meter ({explored}, {total}), meterDone, title, left ({remaining}), leftOne, done, toExplore, exploredGroup, stateUnexplored, stateExplored, revealAll, announceExplore ({label}, {remaining}), announceCleared, announceRevealAll, announceReset ({total})). English defaults.
storageKey / intensity / revealOn / dwell / revealAll / meter / placement / tagPlacementreflectedMirror the attributes.

Methods

NameDescription
explore(id | element, { announce? })Lifts the fog from an area from your code (the user opened the feature elsewhere, finished a task…). Emits mv-explore with source "api"; announce: true also announces it politely. Returns false if unknown, already explored or vetoed.
jump(id)Emits the cancelable mv-fog-jump, then scrolls the area into view, moves focus to it (tabindex="-1" while focused if needed), explores it and shows a short arrival ring. Returns false if unknown or vetoed.
revealAllAreas()Lifts every remaining veil with a short stagger (one mv-explore per area, source "all"), then mv-fog-cleared. Returns how many areas were revealed.
reset()Forgets all progress (and the stored copy) and fades the fog back on every area. Emits mv-fog-reset.
open() / close()Opens or closes the list panel.

Events

NameDescription
mv-exploreCancelable, before an area is revealed. detail: { id, label, source: "pointer" | "focus" | "click" | "view" | "jump" | "api" | "all", explored, total, remaining (counts after this exploration), element }. preventDefault() keeps the fog (e.g. count only clicks for some areas). Save detail.id to your server here.
mv-fog-clearedThe last area was explored (once per cycle, not when the page loads already complete). detail: { total, source }.
mv-fog-jumpCancelable, before the list scrolls to an area. detail: { id, element }. Open the tab, accordion or drawer that holds a hidden area here; the scroll and focus happen on the next frame.
mv-fog-resetreset() was called. detail: { total }.

Content structure

NameDescription
(content)Your interface. Areas are any descendant boxes with data-fog (not display: contents, not table rows or replaced elements such as <input>): a veil <span> is appended inside each unexplored area and removed once it is explored.

CSS classes

NameDescription
mv-fog-veilDecorative overlay inside an unexplored area (aria-hidden, inert, pointer-events: none): wash + backdrop desaturation on the element, grain in ::before, the reveal mask while data-fog-state="revealing".
mv-fog-tagThe “Not explored yet · Label” pill (-state, -name, .mv-fog-ring with its dwell ring), placed by tag-placement. The label is dropped under 22rem of area width (the area usually shows its own title), the text under 9rem, the whole tag under 4rem wide or 2.75rem tall.
mv-fog-meterMeter wrapper: .mv-fog-meter-button (aria-expanded, .mv-fog-dial tile map, a ring above 36 areas, -text, -chevron), data-complete when all areas are explored. .mv-fog-dock is the sticky wrapper used without [data-fog-meter].
mv-fog-panelList panel (popover="auto", top layer): -head (-title, -sub, .mv-fog-bar segments), -body with .mv-fog-group-title and .mv-fog-list of .mv-fog-item buttons (-icon, -label, -hint, -go; data-explored), -foot with .mv-fog-reveal.

CSS variables

NameDefaultDescription
--mv-fog-intensity0.5Veil strength from 0 to 1 (also set by the intensity attribute).
--mv-fog-colorvar(--mv-bg)Tint of the wash: the page color, so the fog reads as distance rather than a warning.
--mv-fog-grainvar(--mv-fg)Color of the grain specks (their opacity follows the intensity).
--mv-fog-accentvar(--mv-accent)Dwell ring, meter segments, check marks and the arrival ring.

Accessibility

The fog is purely visual. Each veil is an aria-hidden, inert span with pointer-events: none, so every control under it stays reachable, clickable and announced exactly as before; nothing is hidden from screen readers and no focus is ever trapped. The text equivalent of the fog is the meter: a real <button> (aria-expanded, aria-controls, aria-haspopup="dialog") whose name is the visible sentence “5 of 12 areas explored”, opening a labelled non-modal dialog (popover="auto": Escape and outside clicks close it, focus returns to the button) that lists areas in two groups, “To explore” and “Explored”; each item is a button whose name includes its state (“Automations, not explored yet”), with Up/Down/Home/End moving between items (roving tabindex). Choosing an item closes the list, scrolls to the area, moves focus to it (tabindex="-1" only while focused, so it never enters the Tab order) and announces “Automations explored. 6 left.” in a polite live region. Announcements are reserved for explicit actions (jump, Reveal all, reset, explore() with announce: true) and the one-time “All 12 areas explored.”; pointer dwell, focus and clicks lift the fog silently so screen reader users are never interrupted by decoration. Keyboard users explore by simply tabbing into an area. Colors are never the only signal: the tag says “Not explored yet”, the list uses dashed-ring vs check icons and words. Reduced motion (OS setting or data-motion="reduce"): no thinning while dwelling, the veil disappears instantly, the arrival ring is static and the panel does not scale. Forced colors: veils are removed entirely and the meter uses system colors (Highlight, GrayText). Veils and the meter are not printed.