App Shell <mv-app-shell>
完全なダッシュボードブロック:折りたたみ可能なサイドバー、固定ヘッダー(パンくずリスト、Ctrl/⌘ + K 検索、未読通知、アカウント)、スパークライン付きの KPI タイル、期間別の売上チャート、アクティビティテーブル。
仕組み
完全なダッシュボードブロック:折りたたみ可能なサイドバー、固定ヘッダー(パンくずリスト、Ctrl/⌘ + K 検索、未読通知、アカウント)、スパークライン付きの KPI タイル、期間別の売上チャート、アクティビティテーブル。レイアウトはコンテナクエリで制御されます。
| カテゴリー | ブロック |
|---|---|
| タイプ | Web Component(<mv-app-shell>) |
| ステータス | 安定版 |
| 同時にインストールされるもの | badge, card, chart, command, dialog, popover, tooltip, sidebar, table |
| Keywords | dashboard, app-shell, layout, admin, kpi, header, command-palette |
When to use
- An admin or SaaS dashboard needs a ready layout with sidebar, sticky header, KPI tiles, a chart and an activity table
- Users expect a Ctrl/Cmd + K search palette, breadcrumbs and unread notifications in the app header
- A back-office prototype must adapt to its container width without separate mobile templates
Avoid when
インストール
node scripts/add.mjs app-shell --out ./src/marvelousMarvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["app-shell"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, components/badge/badge.css, components/card/card.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/chart/chart.js, components/chart/chart.css, components/command/command.js, components/command/command.css, components/dialog/dialog.js, components/dialog/dialog.css, core/focus.js, core/position.js, core/svg.js, components/popover/popover.js, components/popover/popover.css, components/tooltip/tooltip.js, components/tooltip/tooltip.css, components/sidebar/sidebar.js, components/sidebar/sidebar.css, components/table/table.css, components/table/table.js, components/app-shell/app-shell.js, components/app-shell/app-shell.css。
使い方
基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<mv-app-shell id="as-demo">
<div class="mv-sidebar-layout" data-variant="inset" style="height:780px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);overflow:hidden">
<mv-sidebar label="Main navigation" storage-key="mv-demo-app-shell">
<div class="mv-sidebar-header">
<mv-menu placement="bottom-start">
<button class="mv-sidebar-item" data-size="lg" type="button">
<span class="mv-sidebar-logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5l8 7 8-7v14"/></svg></span>
<span class="mv-sidebar-label"><strong>Nova Supply</strong><small>Online store</small></span>
<mv-icon class="mv-sidebar-item-end" name="chevrons-up-down"></mv-icon>
</button>
<div data-content style="--mv-menu-width:15rem">
<div class="mv-menu-label">Workspaces</div>
<div role="group" data-name="workspace">
<button role="menuitemradio" aria-checked="true" value="nova">Nova Supply</button>
<button role="menuitemradio" value="lumen">Studio Lumen</button>
</div>
<hr>
<button role="menuitem" value="new-workspace">New workspace…</button>
</div>
</mv-menu>
</div>
<nav class="mv-sidebar-content" aria-label="Sections">
<div class="mv-sidebar-group">
<h2 class="mv-sidebar-group-label">Store</h2>
<ul class="mv-sidebar-menu">
<li><a class="mv-sidebar-item" href="#as-demo" aria-current="page"><mv-icon name="dashboard"></mv-icon><span class="mv-sidebar-label">Dashboard</span></a></li>
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="shopping-bag"></mv-icon><span class="mv-sidebar-label">Orders</span><span class="mv-sidebar-badge" data-variant="accent">8<span class="mv-sr-only"> to process</span></span></a></li>
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="package"></mv-icon><span class="mv-sidebar-label">Products</span></a></li>
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="users"></mv-icon><span class="mv-sidebar-label">Customers</span></a></li>
<li class="mv-sidebar-sub">
<button class="mv-sidebar-item"><mv-icon name="chart"></mv-icon><span class="mv-sidebar-label">Reports</span></button>
<ul class="mv-sidebar-submenu">
<li><a class="mv-sidebar-subitem" href="#as-demo">Sales</a></li>
<li><a class="mv-sidebar-subitem" href="#as-demo">Traffic</a></li>
<li><a class="mv-sidebar-subitem" href="#as-demo">Retention</a></li>
</ul>
</li>
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="megaphone"></mv-icon><span class="mv-sidebar-label">Marketing</span></a></li>
</ul>
</div>
<div class="mv-sidebar-group" data-push>
<ul class="mv-sidebar-menu">
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="settings"></mv-icon><span class="mv-sidebar-label">Settings</span></a></li>
<li><a class="mv-sidebar-item" href="#as-demo"><mv-icon name="help"></mv-icon><span class="mv-sidebar-label">Help</span></a></li>
</ul>
</div>
</nav>
<div class="mv-sidebar-footer">
<mv-menu placement="right-start">
<button class="mv-sidebar-item" data-size="lg" type="button">
<span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" aria-hidden="true"><span class="mv-avatar-fallback">CR</span></span>
<span class="mv-sidebar-label"><strong>Chloe Reed</strong><small>Admin</small></span>
<mv-icon class="mv-sidebar-item-end" name="chevrons-up-down"></mv-icon>
</button>
<div data-content style="--mv-menu-width:14rem">
<button role="menuitem" value="profile">Profile</button>
<button role="menuitem" value="billing">Billing</button>
<hr>
<button role="menuitem" value="logout">Log out</button>
</div>
</mv-menu>
</div>
</mv-sidebar>
<div class="mv-sidebar-inset">
<header class="mv-app-shell-header">
<button class="mv-button" data-variant="ghost" data-size="icon" data-mv-sidebar-toggle aria-label="Toggle sidebar" title="Sidebar (Ctrl B)">
<mv-icon name="sidebar"></mv-icon>
</button>
<div class="mv-separator" role="separator" aria-orientation="vertical" data-orientation="vertical" style="height:1rem;min-height:1rem;align-self:center"></div>
<nav class="mv-breadcrumb" aria-label="Breadcrumb" data-size="sm">
<ol>
<li><a href="#as-demo">Nova Supply</a></li>
<li><span aria-current="page">Dashboard</span></li>
</ol>
</nav>
<div class="mv-app-shell-actions">
<mv-dialog data-app-shell-search>
<button class="mv-app-shell-search" type="button" data-mv-open aria-label="Search (Ctrl K)" aria-keyshortcuts="Control+K Meta+K /">
<mv-icon name="search"></mv-icon>
<span>Search…</span>
<kbd class="mv-kbd-group"><kbd class="mv-kbd" data-size="sm">Ctrl</kbd><kbd class="mv-kbd" data-size="sm">K</kbd></kbd>
</button>
<dialog class="mv-command-dialog" aria-label="Search">
<mv-command label="Search Nova Supply">
<input placeholder="Search orders, customers, pages…">
<div class="mv-command-list">
<div class="mv-command-group" data-label="Pages">
<div class="mv-command-item" data-value="orders" data-keywords="sales carts">
<mv-icon name="shopping-bag"></mv-icon>
Orders <span class="mv-command-item-meta">8 to process</span>
</div>
<div class="mv-command-item" data-value="customers" data-keywords="contacts buyers">
<mv-icon name="user"></mv-icon>
Customers
</div>
<div class="mv-command-item" data-value="reports" data-keywords="analytics sales traffic">
<mv-icon name="chart"></mv-icon>
Sales reports
</div>
</div>
<div class="mv-command-group" data-label="Actions">
<div class="mv-command-item" data-value="new-product" data-keywords="add create item">
<mv-icon name="plus"></mv-icon>
New product <span class="mv-command-shortcut"><kbd>N</kbd></span>
</div>
<div class="mv-command-item" data-value="export" data-keywords="csv download">
<mv-icon name="download"></mv-icon>
Export this month’s sales
</div>
</div>
</div>
<div class="mv-command-empty">No results.</div>
</mv-command>
</dialog>
</mv-dialog>
<mv-popover placement="bottom-end">
<button class="mv-button mv-badge-anchor" data-variant="ghost" data-size="icon" data-app-shell-unread-label="Notifications" aria-label="Notifications">
<mv-icon name="bell"></mv-icon>
<span class="mv-badge" data-variant="danger" data-appearance="solid" data-shape="pill" data-size="sm" data-position="top-right" style="--mv-badge-offset:4px" data-app-shell-unread aria-hidden="true">3</span>
</button>
<div data-content style="--mv-popover-width:22rem">
<div class="mv-app-shell-notifs">
<div class="mv-app-shell-notifs-head">
<h3>Notifications</h3>
<button class="mv-button" data-variant="ghost" data-size="sm" data-app-shell-read-all>Mark all as read</button>
</div>
<ul>
<li><a class="mv-app-shell-notif" href="#as-demo" data-unread><span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:40" aria-hidden="true"><span class="mv-avatar-fallback">JD</span></span><span><p><strong>James Davis</strong> placed an order for $214.90</p><time datetime="2026-09-23T10:42">4 min ago</time></span></a></li>
<li><a class="mv-app-shell-notif" href="#as-demo" data-unread><span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:160" aria-hidden="true"><span class="mv-avatar-fallback">MC</span></span><span><p><strong>Mateo Cruz</strong> left a 5-star review on “Eclipse Lamp”</p><time datetime="2026-09-23T09:15">1 hr ago</time></span></a></li>
<li><a class="mv-app-shell-notif" href="#as-demo" data-unread><span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:20" aria-hidden="true"><span class="mv-avatar-fallback">!</span></span><span><p><strong>Low stock:</strong> only 3 “Opal Vase” left</p><time datetime="2026-09-23T08:02">2 hr ago</time></span></a></li>
<li><a class="mv-app-shell-notif" href="#as-demo"><span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:280" aria-hidden="true"><span class="mv-avatar-fallback">IM</span></span><span><p><strong>Imani Moore</strong> joined the team</p><time datetime="2026-09-22T17:30">Yesterday</time></span></a></li>
</ul>
<div class="mv-app-shell-notifs-foot"><a href="#as-demo">View all notifications</a></div>
</div>
</div>
</mv-popover>
<mv-menu placement="bottom-end">
<button class="mv-button" data-variant="ghost" data-size="icon" aria-label="My account" style="border-radius:var(--mv-radius-full)">
<span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:200" aria-hidden="true"><span class="mv-avatar-fallback">CR</span></span>
</button>
<div data-content style="--mv-menu-width:14rem">
<div class="mv-menu-label">[email protected]</div>
<button role="menuitem" value="profile">Profile</button>
<button role="menuitem" value="settings">Settings <span class="mv-menu-shortcut">⌘,</span></button>
<hr>
<button role="menuitem" value="logout">Log out</button>
</div>
</mv-menu>
</div>
</header>
<main class="mv-app-shell-main" aria-labelledby="as-demo-title">
<div class="mv-app-shell-heading">
<div>
<h1 class="mv-app-shell-title" id="as-demo-title">Dashboard</h1>
<p class="mv-app-shell-subtitle">Hi Chloe, here’s how the store did in September.</p>
</div>
<div class="mv-app-shell-toolbar">
<button class="mv-button" data-variant="outline" data-size="sm">
<mv-icon name="download"></mv-icon>
Export
</button>
<button class="mv-button" data-size="sm">
<mv-icon name="plus"></mv-icon>
New product
</button>
</div>
</div>
<section class="mv-app-shell-stats" aria-label="Key metrics">
<article class="mv-card mv-app-shell-stat">
<div class="mv-app-shell-stat-head"><span>Revenue</span><mv-icon name="dollar"></mv-icon></div>
<div class="mv-app-shell-stat-value">$48,290</div>
<div class="mv-app-shell-stat-foot"><span class="mv-badge" data-variant="success" data-size="sm">▲ 12.4%</span> vs Aug</div>
<mv-chart sparkline type="area" label="Revenue, last 12 weeks" format="currency" data='{"labels":["W27","W28","W29","W30","W31","W32","W33","W34","W35","W36","W37","W38"],"series":[{"name":"Revenue","values":[8100,8900,8400,9600,9100,10200,9800,10900,11400,10800,11900,12600]}]}'></mv-chart>
</article>
<article class="mv-card mv-app-shell-stat">
<div class="mv-app-shell-stat-head"><span>New customers</span><mv-icon name="user-plus"></mv-icon></div>
<div class="mv-app-shell-stat-value">1,284</div>
<div class="mv-app-shell-stat-foot"><span class="mv-badge" data-variant="success" data-size="sm">▲ 8.1%</span> vs Aug</div>
<mv-chart sparkline type="area" label="New customers, last 12 weeks" data='{"labels":["W27","W28","W29","W30","W31","W32","W33","W34","W35","W36","W37","W38"],"series":[{"name":"Customers","values":[240,262,251,270,288,276,301,297,312,305,322,331]}]}'></mv-chart>
</article>
<article class="mv-card mv-app-shell-stat">
<div class="mv-app-shell-stat-head"><span>Orders</span><mv-icon name="shopping-bag"></mv-icon></div>
<div class="mv-app-shell-stat-value">3,912</div>
<div class="mv-app-shell-stat-foot"><span class="mv-badge" data-variant="danger" data-size="sm">▼ 2.3%</span> vs Aug</div>
<mv-chart sparkline type="area" label="Orders, last 12 weeks" data='{"labels":["W27","W28","W29","W30","W31","W32","W33","W34","W35","W36","W37","W38"],"series":[{"name":"Orders","values":[1010,1042,998,1065,1031,1002,987,1019,972,961,948,936]}]}'></mv-chart>
</article>
<article class="mv-card mv-app-shell-stat">
<div class="mv-app-shell-stat-head"><span>Conversion rate</span><mv-icon name="trending-up"></mv-icon></div>
<div class="mv-app-shell-stat-value">3.42%</div>
<div class="mv-app-shell-stat-foot"><span class="mv-badge" data-variant="success" data-size="sm">▲ 0.4 pt</span> vs Aug</div>
<mv-chart sparkline type="area" label="Conversion rate, last 12 weeks" format="percent" data='{"labels":["W27","W28","W29","W30","W31","W32","W33","W34","W35","W36","W37","W38"],"series":[{"name":"Conversion","values":[2.9,3.0,2.95,3.1,3.05,3.2,3.15,3.28,3.3,3.25,3.38,3.42]}]}'></mv-chart>
</article>
</section>
<div class="mv-app-shell-row">
<article class="mv-card">
<header class="mv-card-header mv-app-shell-card-head">
<div>
<h2 class="mv-card-title">Sales</h2>
<p class="mv-card-description" id="as-demo-range-desc">Last 30 days, in USD</p>
</div>
<mv-tabs data-variant="segmented" data-size="sm" value="30" id="as-demo-range">
<div role="tablist" aria-label="Period">
<button role="tab" data-value="7">7d</button>
<button role="tab" data-value="30">30d</button>
<button role="tab" data-value="90">3 mo</button>
</div>
</mv-tabs>
</header>
<div class="mv-card-content">
<mv-chart id="as-demo-chart" type="area" format="currency" height="250" label="Daily sales"></mv-chart>
</div>
</article>
<article class="mv-card">
<header class="mv-card-header">
<h2 class="mv-card-title">Traffic sources</h2>
<p class="mv-card-description">24,812 visits this month</p>
</header>
<div class="mv-card-content">
<ul class="mv-app-shell-breakdown">
<li><span>Organic search</span><strong>42%<small> 10,421</small></strong><progress class="mv-progress" data-size="sm" max="100" value="42" aria-label="Organic search, 42%"></progress></li>
<li><span>Direct</span><strong>27%<small> 6,699</small></strong><progress class="mv-progress" data-size="sm" max="100" value="27" aria-label="Direct, 27%"></progress></li>
<li><span>Social media</span><strong>18%<small> 4,466</small></strong><progress class="mv-progress" data-size="sm" max="100" value="18" aria-label="Social media, 18%"></progress></li>
<li><span>Newsletter</span><strong>13%<small> 3,226</small></strong><progress class="mv-progress" data-size="sm" max="100" value="13" aria-label="Newsletter, 13%"></progress></li>
</ul>
</div>
</article>
</div>
<article class="mv-card">
<header class="mv-card-header">
<h2 class="mv-card-title" id="as-demo-orders-title">Recent orders</h2>
<p class="mv-card-description">5 of 3,912 orders this month</p>
<div class="mv-card-action"><a class="mv-button" data-variant="outline" data-size="sm" href="#as-demo">View all</a></div>
</header>
<div class="mv-card-content">
<div class="mv-table-wrap" role="region" aria-labelledby="as-demo-orders-title" tabindex="0">
<table class="mv-table" data-hover aria-labelledby="as-demo-orders-title">
<thead>
<tr><th scope="col">Customer</th><th scope="col" data-app-shell-hide="sm">Order</th><th scope="col">Status</th><th scope="col" data-app-shell-hide="sm">Date</th><th scope="col" data-align="end">Amount</th></tr>
</thead>
<tbody>
<tr><td><span class="mv-app-shell-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:40" aria-hidden="true"><span class="mv-avatar-fallback">JD</span></span><span><strong>James Davis</strong><small>[email protected]</small></span></span></td><td data-app-shell-hide="sm">#10492</td><td><span class="mv-table-status" data-status="success">Paid</span></td><td data-app-shell-hide="sm">Sep 23</td><td data-align="end">$214.90</td></tr>
<tr><td><span class="mv-app-shell-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" aria-hidden="true"><span class="mv-avatar-fallback">MC</span></span><span><strong>Mateo Cruz</strong><small>[email protected]</small></span></span></td><td data-app-shell-hide="sm">#10491</td><td><span class="mv-table-status" data-status="info">Shipped</span></td><td data-app-shell-hide="sm">Sep 23</td><td data-align="end">$89.00</td></tr>
<tr><td><span class="mv-app-shell-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:320" aria-hidden="true"><span class="mv-avatar-fallback">SN</span></span><span><strong>Sofia Nguyen</strong><small>[email protected]</small></span></span></td><td data-app-shell-hide="sm">#10490</td><td><span class="mv-table-status" data-status="warning">Pending</span></td><td data-app-shell-hide="sm">Sep 22</td><td data-align="end">$1,249.00</td></tr>
<tr><td><span class="mv-app-shell-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:240" aria-hidden="true"><span class="mv-avatar-fallback">TG</span></span><span><strong>Tomás García</strong><small>[email protected]</small></span></span></td><td data-app-shell-hide="sm">#10489</td><td><span class="mv-table-status" data-status="danger">Refunded</span></td><td data-app-shell-hide="sm">Sep 22</td><td data-align="end">$64.50</td></tr>
<tr><td><span class="mv-app-shell-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:100" aria-hidden="true"><span class="mv-avatar-fallback">LM</span></span><span><strong>Leila Mansour</strong><small>[email protected]</small></span></span></td><td data-app-shell-hide="sm">#10488</td><td><span class="mv-table-status" data-status="success">Paid</span></td><td data-app-shell-hide="sm">Sep 21</td><td data-align="end">$372.00</td></tr>
</tbody>
</table>
</div>
</div>
</article>
</main>
</div>
</div>
</mv-app-shell>
<script type="module">
const shell = document.getElementById("as-demo");
const chart = shell?.querySelector("#as-demo-chart");
const desc = shell?.querySelector("#as-demo-range-desc");
// Deterministic pseudo-random series so the demo is stable between reloads.
const series = (n, seed, base) => {
let s = seed;
const rnd = () => ((s = (s * 16807) % 2147483647) / 2147483647);
return Array.from({ length: n }, (_, i) => Math.round(base + i * base * 0.012 + (rnd() - 0.4) * base * 0.45 + Math.sin(i / 2.2) * base * 0.12));
};
const day = (offset) => new Date(Date.UTC(2026, 8, 23 - offset)).toLocaleDateString("en-US", { day: "numeric", month: "short", timeZone: "UTC" });
const ranges = {
7: { labels: Array.from({ length: 7 }, (_, i) => day(6 - i)), cur: series(7, 11, 1650), prev: series(7, 5, 1420), text: "Last 7 days, in USD" },
30: { labels: Array.from({ length: 30 }, (_, i) => day(29 - i)), cur: series(30, 7, 1450), prev: series(30, 3, 1300), text: "Last 30 days, in USD" },
90: { labels: Array.from({ length: 13 }, (_, i) => `W${26 + i}`), cur: series(13, 9, 10200), prev: series(13, 2, 9100), text: "Last 13 weeks, in USD" },
};
const show = (key) => {
const r = ranges[key];
if (!chart || !r) return;
chart.data = { labels: r.labels, series: [{ name: "This period", values: r.cur }, { name: "Previous period", values: r.prev }] };
if (desc) desc.textContent = r.text;
};
show("30");
shell?.querySelector("#as-demo-range")?.addEventListener("mv-change", (e) => show(e.detail.value));
</script>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
hotkey | string | k | Key combined with Ctrl/⌘ to open search (“/” also opens it outside text fields); empty to disable. |
no-skip-link | boolean | false | Do not create the “Skip to content” link (when the page already has one). A link you write yourself as a.mv-app-shell-skip is kept. |
Properties
| Name | タイプ | Description |
|---|---|---|
strings | Partial<Record<string, string | { one, other }>> | Overrides for the default text: skip (skip link), unread (bell label, {label} and {n}, plural forms as { one, other }), unreadItem (hidden prefix read before an unread notification). Numbers and plurals follow the nearest lang (Intl). English defaults. Can be set before the element is defined. |
Methods
| Name | Description |
|---|---|
openSearch() | Opens the search palette. |
Events
| Name | Description |
|---|---|
mv-navigate | Command picked in the palette (which then closes). detail: { value, label }. |
Content structure
| Name | Description |
|---|---|
mv-dialog[data-app-shell-search] | Search palette (a .mv-command-dialog dialog containing an <mv-command>). |
[data-unread] | Unread notification (dot); clicking marks it as read. |
[data-app-shell-read-all] | “Mark all as read” button. |
[data-app-shell-unread] | Unread counter, hidden at 0. |
[data-app-shell-unread-label] | On the bell button: its aria-label becomes “<value>, N unread”. |
CSS classes
| Name | Description |
|---|---|
mv-app-shell-skip | Skip link, first in the tab order, visible on focus; created automatically, it moves focus to .mv-app-shell-main (or <main>) without changing the URL. |
mv-app-shell-header / -actions / -search | Sticky blurred header, right-hand action area, fake search field (icon only in narrow containers). |
mv-app-shell-main / -heading / -title / -subtitle / -toolbar | Content (mv-app-shell query container), page title and actions. |
mv-app-shell-stats / -stat / -stat-head / -stat-value / -stat-foot | 1 → 2 → 4 column grid (34 / 56rem) of KPI tiles. |
mv-app-shell-row | 2/3 + 1/3 row above 56rem. |
mv-app-shell-notifs / -notif | Notification list inside a popover. |
mv-app-shell-breakdown / -person / -card-head | Bar breakdown, customer cell (avatar + name), card header with actions. |
[data-app-shell-hide="sm"] | Column hidden below a 40rem container. |
Accessibility
Skip link to the main content, first in the tab order. Landmarks: navigation region, header, <main> named by the page title. Search: button with aria-keyshortcuts, modal dialog (focus trap, Esc, focus returned). Bell: aria-label updated with the unread count (Intl plurals and numbers); unread notifications get a hidden “Unread:” prefix. KPIs: named section; accessible charts (data table). Tables with column headers, named by their card title. Forced colors: focus and unread dots drawn with system colors.