エクスクルーシブ
Fog <mv-fog>
情報量の多いインターフェース(管理コンソール、プロ向けツール、多数のパネルを持つ分析アプリ、深い設定画面)のための段階的な発見の仕組みです。data-fog が付いた、ユーザーがまだ探索していない領域は、軽い質感のあるベール(ページ色のもや、柔らかな雲の模様、わずかな彩度の低下、細かい粒子。すべて CSS で canvas は不要)の下に置かれ、小さな「Not explored yet · Automations」タグが付きます。中身は読むこともクリックすることもでき、支援技術にも完全に公開されます。
文化的リファレンス
シヴィライゼーション、シド・マイヤー & ブルース・シェリー(MicroProse)(1991年、ゲーム)。マップは戦場の霧に覆われた状態で始まり、ユニットが探索した場所の周囲だけが明らかになり、探索済みの土地はずっと明らかなままです。UI では、ユーザーがまだ探索していない領域は薄いベールの下にあり、実際に操作した場所でベールが晴れ、セッションをまたいでも晴れたままになります。インターフェースのうちどれだけがまだ未知かは、メーターが示します。
仕組み
高密度なインターフェース(管理コンソール、プロ向けツール、多数のパネルを持つ分析アプリ、深い階層の設定)のための段階的な発見。data-fog が付いた領域のうち、ユーザーがまだ探索していないものはすべて、薄く質感のあるベール(ページ色のもや、柔らかな雲の斑、わずかな彩度低下と細かな粒子。すべて CSS で、canvas は使いません)の下に置かれ、小さな「Not explored yet · Automations」タグが付きます。その間もコンテンツは読むことができ、クリックでき、支援技術にも完全に公開されます。領域にポインターを置いたままにすると霧が薄くなり、dwell ミリ秒をかけて小さなリングが満ち、その後ベールは操作したまさにその地点から開いていく晴れ間とともに消えます。キーボードフォーカス、領域内での押下、オプションのスクロール表示ルール、コードからの explore(id) でも霧は晴れます。探索済みの領域が再び霧に覆われることはありません。進捗は storage-key で保持される(タブ間で同期)か、explored プロパティを通じてサーバーから取得します。メーター(「5 of 12 areas explored」と、領域ごとに 1 タイルの小さなマップ)を開くと残りの領域がヒント付きで一覧表示され、そこから領域へスクロールしてフォーカスできます。オプションの「Reveal all」はエキスパート向けで、reset() で霧を元に戻せます。mv-explore はキャンセル可能で(滞在による発見を拒否し、実際の使用だけを数えられます)、mv-fog-jump を使えば隠れた領域を含むタブを開くことができ、mv-fog-cleared はすべてが既知になったときに一度だけ発火します。intensity でベールの濃さを調整でき、赤く表示されたりクリックを妨げたりすることは決してありません。
| カテゴリー | レイアウト |
|---|---|
| タイプ | Web Component(<mv-fog>) |
| ステータス | 安定版 |
| キット | パワーユーザー向けナビゲーション |
| 同時にインストールされるもの | 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
インストール
node scripts/add.mjs fog --out ./src/marvelousMarvelous UI MCP サーバーを使う AI エージェント:install_components({ slugs: ["fog"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, components/button/button.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。
使い方
クイックスタート、動作する最小限のマークアップ:
<mv-fog storage-key="console">
<section data-fog="orders">Orders…</section>
<section data-fog="automations">Automations…</section>
</mv-fog>基本となるマークアップです。ここから属性、data-*、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 chat</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>API
Attributes
| Name | タイプ | 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 | タイプ | 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; tabbing out of it closes it and leaves focus where it went) 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.