익스클루시브
Fog <mv-fog>
복잡한 인터페이스(관리 콘솔, 전문가용 도구, 패널이 많은 분석 앱, 깊은 설정 화면)를 위한 점진적 탐색 기능입니다. data-fog로 표시한 영역 중 사용자가 아직 살펴보지 않은 곳은 가볍고 질감 있는 베일(페이지 색상의 안개, 부드러운 구름 조각, 약간의 채도 감소, 미세한 그레인으로 모두 CSS이며 canvas 없음)과 작은 “Not explored yet · Automations” 태그 아래에 놓이지만, 콘텐츠는 그대로 읽고 클릭할 수 있으며 보조 기술에도 완전히 노출됩니다.
문화적 레퍼런스
시드 마이어의 문명, 시드 마이어 & 브루스 셸리(MicroProse)(1991년, 게임). 지도는 전장의 안개에 덮인 채 시작해 유닛이 탐험한 곳 주변만 드러나고, 탐험한 땅은 영원히 드러난 채로 남습니다. UI에서는 사용자가 아직 탐색하지 않은 영역이 옅은 베일 아래에 놓이고, 실제로 상호작용한 곳에서 베일이 걷히며, 세션이 바뀌어도 걷힌 상태가 유지되고, 미터가 인터페이스 중 아직 모르는 부분이 얼마나 되는지 보여 줍니다.
작동 방식
밀도 높은 인터페이스(관리 콘솔, 전문 도구, 패널이 많은 분석 앱, 깊은 설정)를 위한 점진적 발견: data-fog로 표시된 영역 중 사용자가 아직 탐색하지 않은 곳은 질감 있는 옅은 베일(페이지 색의 안개, 부드러운 구름 조각, 약간의 채도 감소와 고운 그레인, 모두 CSS이며 캔버스 없음) 아래에 놓이고 작은 “Not explored yet · Automations” 태그가 붙지만, 콘텐츠는 그대로 읽을 수 있고 클릭할 수 있으며 보조 기술에도 완전히 노출됩니다. 포인터를 영역 위에 두면 안개가 옅어지며 dwell ms 동안 작은 링이 채워지고, 이어서 상호작용한 바로 그 지점에서 열리는 빈터처럼 베일이 걷힙니다. 키보드 포커스, 영역 안에서의 누름, 선택적 scroll-into-view 규칙, 또는 코드의 explore(id)로도 베일을 걷을 수 있습니다. 탐색한 영역은 다시는 안개에 덮이지 않습니다: 진행 상황은 storage-key로 유지되거나(탭 간 동기화) explored 속성을 통해 서버에서 가져옵니다. 미터(영역마다 타일 하나가 있는 작은 지도 옆의 “5 of 12 areas explored”)는 남은 영역 목록을 힌트와 함께 열고, 목록에서 고른 영역으로 스크롤해 포커스를 옮깁니다. 선택적인 “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.