Exclusivobeta
Fog <mv-fog>
Descoberta progressiva para interfaces densas (consoles de administração, ferramentas profissionais, apps de analytics com muitos painéis, configurações profundas): cada área marcada com data-fog que o usuário ainda não explorou fica sob um véu leve e texturizado (uma névoa da cor da página, manchas suaves de nuvem, uma leve dessaturação e um grão fino, tudo em CSS, sem canvas) com uma pequena etiqueta “Not explored yet · Automations”, enquanto seu conteúdo continua legível, clicável e totalmente exposto às tecnologias assistivas. Deixar o ponteiro parado sobre uma área afina a névoa e preenche um pequeno anel ao longo de dwell ms, e então o véu se levanta em uma clareira que se abre a partir do ponto exato da interação; o foco do teclado, um clique dentro, uma regra opcional de entrada na vista ou explore(id) a partir do seu código também o levantam. As áreas exploradas nunca voltam a ficar sob a névoa: o progresso persiste com storage-key (sincronizado entre abas) ou vem do seu servidor pela propriedade explored. Um medidor (“5 of 12 areas explored” ao lado de um minúsculo mapa com um bloco por área) abre uma lista do que falta, com dicas, que rola até uma área e a foca; um “Reveal all” opcional atende os especialistas e reset() traz a névoa de volta. mv-explore é cancelável (vete a descoberta por permanência do ponteiro, conte só o uso real), mv-fog-jump permite abrir a aba que contém uma área oculta, mv-fog-cleared dispara quando tudo já é conhecido. intensity ajusta a intensidade do véu, e nada nunca fica vermelho nem bloqueia um clique.
| Categoria | Layout |
|---|---|
| Tipo | Web Component (<mv-fog>) |
| Status | beta |
| Kit | Navegação para usuários avançados |
| Também instala | button |
| Keywords | exclusive, culture, progressive-disclosure, discovery, onboarding, feature-discovery, exploration, fog-of-war, adoption, admin, dashboard, veil, progress, coach, persistence |
When to use
- A console or pro tool has many panels and new users never find the ones that matter, such as automations, webhooks or the audit log
- Product wants to see which areas each user has really used and nudge them toward the rest without a blocking tour
- A redesigned screen should quietly mark its new or moved sections until each user has looked at them once
- Onboarding progress must survive reloads and devices, driven by explored ids stored on the server
Avoid when
- Users must be walked through the steps in a fixed order, not left to discover areas freely → use Stepper instead
- The user is stuck on one task and needs escalating help to finish it → use Insist instead
- Progress is a collection of features or achievements to browse as a grid, not areas of the live interface → use Field Guide instead
Instalação
node scripts/add.mjs fog --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["fog"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): 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.
Uso
Início rápido, a menor marcação que funciona:
<mv-fog storage-key="console">
<section data-fog="orders">Orders…</section>
<section data-fog="automations">Automations…</section>
</mv-fog>Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<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 < 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>Referência cultural
Sid Meier's Civilization, Sid Meier & Bruce Shelley (MicroProse) (1991, jogo). O mapa começa sob uma névoa de guerra e só se revela em volta dos lugares que as suas unidades exploraram, e o terreno explorado fica revelado para sempre. Na interface, as áreas que o usuário ainda não explorou ficam sob um véu leve que se levanta onde ele realmente interage, continua levantado de uma sessão para outra, e um medidor mostra quanto da interface ainda é desconhecido.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
storage-key | string | Persists 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(). | |
explored | comma-separated ids | Areas already explored when the page loads (e.g. rendered by your server). Setting it later replaces the explored set without events or animation. | |
intensity | number (0..1) | 0.5 | Strength of the veil (wash, desaturation, blur and grain all scale with it). Sets --mv-fog-intensity on the element. |
reveal-on | space-separated: pointer | focus | click | view | pointer focus click | What 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. |
dwell | number (ms) | 700 | How 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-all | boolean | false | Shows a “Reveal all” button in the list panel (experts, returning users). |
meter | "auto" | "none" | auto | auto 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-placement | top-end | top-start | bottom-end | bottom-start | center | none | top-end | Corner of each veil where the “Not explored yet” tag sits (most panels keep secondary metadata top end). none keeps the veil without a tag. |
placement | top | bottom | left | right, with -start | -end | top-start | Preferred side of the list panel relative to the meter button (flips and shifts to stay in the viewport). |
data-fog | string (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-hint | string | On 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-state | veiled | clearing | revealing | explored | Set 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-meter | empty element | Placeholder where the meter is rendered (sidebar footer, toolbar…). |
Properties
| Name | Tipo | Description |
|---|---|---|
explored | string[] | 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. |
areas | Array<{ 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. |
strings | Partial<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 / tagPlacement | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-explore | Cancelable, 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-cleared | The last area was explored (once per cycle, not when the page loads already complete). detail: { total, source }. |
mv-fog-jump | Cancelable, 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-reset | reset() was called. detail: { total }. |
Content structure
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-fog-veil | Decorative 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-tag | The “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-meter | Meter 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-panel | List 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
| Name | Default | Description |
|---|---|---|
--mv-fog-intensity | 0.5 | Veil strength from 0 to 1 (also set by the intensity attribute). |
--mv-fog-color | var(--mv-bg) | Tint of the wash: the page color, so the fog reads as distance rather than a warning. |
--mv-fog-grain | var(--mv-fg) | Color of the grain specks (their opacity follows the intensity). |
--mv-fog-accent | var(--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.