Exclusivobeta
Keen Sight <mv-keen-sight>
Modo de dicas revelado ao segurar uma tecla, que permite a quem usa teclado agir sobre qualquer coisa que esteja vendo sem percorrê-la com Tab. Segure Alt (Option no Mac) por um quarto de segundo sem outra tecla, pressione uma combinação de ativação ou clique em um botão de ativação: a região (ou a página inteira com global) perde a saturação e escurece sob um véu com um recorte para cada elemento acionável realmente à vista (links, botões, campos, widgets ARIA, o seu próprio seletor), cada um contornado e rotulado com uma dica de 1-2 letras. As dicas não compartilham prefixos e as mais curtas vêm primeiro (letras da fileira central para os primeiros alvos), e o formato do badge diz o que vai acontecer antes de você digitar: uma etiqueta para navegação, um bloco para ações, um campo sublinhado para entradas, um octógono para controles destrutivos. Digitar uma dica foca o elemento e clica nele (os campos só são focados; Shift na última letra só move o foco), Backspace corrige, e uma letra que não corresponde é recusada na hora. Os elementos que declaram o próprio atalho (aria-keyshortcuts, data-shortcut ou data-combo, exibido conforme a plataforma como ⌘⇧E ou Ctrl+Shift+E, e as sequências como G · N) mostram esse atalho em vez de uma dica, então o modo também ensina os atalhos reais. Soltar a tecla, Escape, um clique ou qualquer outra tecla sai do modo; nenhuma tecla de caractere único é capturada enquanto o modo está fechado. Os alvos são encontrados em uma única passada de leitura (retângulos mais um teste de acerto, então tudo o que estiver coberto por um diálogo, um cabeçalho fixo ou a borda de um contêiner com rolagem é ignorado) e escritos em uma única passada, recalculados na rolagem, no redimensionamento e nas mudanças do DOM, com as dicas mantidas estáveis entre os recálculos; mv-hint-open e mv-hint-activate são canceláveis.
| Categoria | Navegação |
|---|---|
| Tipo | Web Component (<mv-keen-sight>) |
| Status | beta |
| Kit | Navegação para usuários avançados |
| Keywords | exclusive, culture, keyboard, hint-mode, link-hints, hints, shortcuts, hotkeys, discoverability, power-user, accessibility, navigation, overlay, spotlight, aria-keyshortcuts, focus |
When to use
- A dense web app (back office, tracker, admin table) should let keyboard users reach any visible control in two keystrokes
- New users need to discover at a glance what is clickable on a busy screen and which controls already have shortcuts
- A toolbar or dashboard has too many controls for Tab order to be practical, but adding a shortcut to each one is not
- Existing aria-keyshortcuts or declared shortcuts should be surfaced in context instead of in a separate cheat sheet
Avoid when
- Users need memorized multi-key shortcuts bound to specific actions rather than hints generated for what is on screen → use Combo instead
- Users look for an action by name, including actions that are not visible on the screen → use Command instead
- The page is mostly free text entry (an editor, a chat) where holding Alt or Option is already used for typing or word navigation
Instalação
node scripts/add.mjs keen-sight --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["keen-sight"], 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/motion.js, components/keen-sight/keen-sight.js, components/keen-sight/keen-sight.css.
Uso
Início rápido, a menor marcação que funciona:
<mv-keen-sight>
<nav><a href="/invoices">Invoices</a> <button>New invoice</button> <input type="search" aria-label="Search"></nav>
</mv-keen-sight>Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div id="ks-demo" style="width:min(100%,66rem);margin-inline:auto">
<style>
#ks-demo .ks-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; margin:0 0 .875rem }
#ks-demo .ks-bar-start, #ks-demo .ks-bar-end { display:flex; align-items:center; gap:.75rem 1rem; flex-wrap:wrap }
#ks-demo .ks-how { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.6 }
#ks-demo .ks-field { display:flex; align-items:center; gap:.5rem; font-size:.8125rem; color:var(--mv-fg-muted) }
#ks-demo .ks-field .mv-select { width:auto; min-width:10.5rem }
#ks-demo [data-keen-sight-toggle][aria-pressed="true"] { box-shadow:0 0 0 3px var(--mv-accent-subtle) }
#ks-demo .ks-log { display:flex; align-items:center; gap:.5rem; min-height:1.75rem; margin:.875rem 0 0; font-size:.8125rem; color:var(--mv-fg-muted) }
#ks-demo .ks-log b { color:var(--mv-fg); font-weight:600 }
#ks-demo .ks-app { display:grid; grid-template-columns:12.5rem minmax(0,1fr); min-height:33rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden; font-size:.875rem }
#ks-demo .ks-side { display:flex; flex-direction:column; gap:1rem; padding:1rem .75rem; border-inline-end:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#ks-demo .ks-brand { display:flex; align-items:center; gap:.5rem; padding:.25rem .5rem; font-weight:650; letter-spacing:-.01em }
#ks-demo .ks-logo { display:grid; place-items:center; width:1.625rem; height:1.625rem; border-radius:var(--mv-radius-md); background:var(--mv-fg); color:var(--mv-bg); font-size:.75rem; font-weight:700 }
#ks-demo .ks-nav { display:grid; gap:.125rem }
#ks-demo .ks-nav a { display:flex; align-items:center; gap:.625rem; padding:.4375rem .625rem; border-radius:var(--mv-radius-md); color:var(--mv-fg-muted); text-decoration:none }
#ks-demo .ks-nav a:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
#ks-demo .ks-nav a[aria-current] { background:var(--mv-surface); color:var(--mv-fg); font-weight:550; box-shadow:var(--mv-shadow-xs) }
#ks-demo .ks-nav a:focus-visible, #ks-demo .ks-link:focus-visible, #ks-demo .ks-filters a:focus-visible, #ks-demo .ks-pager a:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
#ks-demo .ks-nav svg { width:1rem; height:1rem; flex:none }
#ks-demo .ks-side-foot { margin-top:auto; padding:.75rem .625rem 0; border-top:1px solid var(--mv-border); color:var(--mv-fg-subtle); font-size:.75rem; line-height:1.5 }
#ks-demo .ks-main { display:flex; flex-direction:column; gap:1rem; padding:1.25rem 1.5rem; min-width:0 }
#ks-demo .ks-head { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; flex-wrap:wrap }
#ks-demo .ks-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.015em }
#ks-demo .ks-head p { margin:.1875rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#ks-demo .ks-actions { display:flex; gap:.5rem }
#ks-demo .ks-tools { display:flex; align-items:center; gap:.625rem; flex-wrap:wrap }
#ks-demo .ks-tools .mv-input { flex:1 1 14rem; min-width:0 }
#ks-demo .ks-tools .mv-select { width:auto; flex:none }
#ks-demo .ks-filters { display:flex; gap:.25rem; margin-inline-start:auto }
#ks-demo .ks-filters a { display:inline-flex; align-items:center; gap:.375rem; height:2rem; padding:0 .625rem; border-radius:var(--mv-radius-full); color:var(--mv-fg-muted); font-size:.8125rem; text-decoration:none }
#ks-demo .ks-filters a:hover { color:var(--mv-fg) }
#ks-demo .ks-filters a[aria-current] { background:var(--mv-bg-muted); color:var(--mv-fg); font-weight:550 }
#ks-demo .ks-filters small { color:var(--mv-fg-subtle); font-size:.75rem; font-variant-numeric:tabular-nums }
#ks-demo .ks-table-wrap { overflow-x:auto; margin-inline:-.25rem; padding-inline:.25rem }
#ks-demo .ks-table { width:100%; border-collapse:collapse; font-size:.8125rem }
#ks-demo .ks-table th { padding:.5rem .625rem; border-bottom:1px solid var(--mv-border); color:var(--mv-fg-muted); font-weight:500; font-size:.75rem; text-align:start; white-space:nowrap }
#ks-demo .ks-table td { padding:.5rem .625rem; border-bottom:1px solid var(--mv-border); vertical-align:middle; white-space:nowrap }
#ks-demo .ks-table tr:last-child td { border-bottom:0 }
#ks-demo .ks-table .ks-num { text-align:end; font-variant-numeric:tabular-nums }
#ks-demo .ks-table .ks-check { width:2rem }
#ks-demo .ks-table .ks-row-actions { display:flex; justify-content:flex-end; gap:.25rem }
#ks-demo .ks-link { color:var(--mv-fg); font-weight:550; text-decoration:none; border-radius:var(--mv-radius-xs); font-family:var(--mv-font-mono); font-size:.75rem }
#ks-demo .ks-link:hover { text-decoration:underline }
#ks-demo .ks-vendor { color:var(--mv-fg-muted) }
#ks-demo .ks-del { --_h:1.75rem }
#ks-demo .ks-del svg { font-size:.9375rem }
#ks-demo tr[data-removing] { opacity:.35; transition:opacity var(--mv-duration-normal) }
#ks-demo .ks-foot { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-top:auto; color:var(--mv-fg-muted); font-size:.75rem }
#ks-demo .ks-pager { display:flex; gap:.25rem }
#ks-demo .ks-pager a { display:inline-flex; align-items:center; height:1.75rem; padding:0 .625rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); color:var(--mv-fg); text-decoration:none }
#ks-demo .ks-pager a[aria-disabled="true"] { color:var(--mv-fg-subtle); pointer-events:none }
@media (max-width:52rem) {
#ks-demo .ks-app { grid-template-columns:minmax(0,1fr) }
#ks-demo .ks-side { display:none }
#ks-demo .ks-filters { margin-inline-start:0 }
#ks-demo .ks-hide-sm { display:none }
}
</style>
<div class="ks-bar">
<div class="ks-bar-start">
<button class="mv-button" type="button" data-size="sm" data-keen-sight-toggle="ks-app" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
Hint mode
</button>
<p class="ks-how">or hold <kbd class="mv-kbd" id="ks-key">Alt</kbd> over the app, type a hint, release to exit</p>
</div>
<div class="ks-bar-end">
<label class="ks-field" for="ks-shortcuts">Declared shortcuts
<select class="mv-select" id="ks-shortcuts" data-size="sm">
<option value="replace" selected>Instead of hints</option>
<option value="both">Next to hints</option>
<option value="ignore">Hidden</option>
</select>
</label>
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ks-sticky">
<span class="mv-choice-text"><span class="mv-choice-title">Sticky</span></span>
</label>
</div>
</div>
<mv-keen-sight id="ks-app" open>
<div class="ks-app">
<aside class="ks-side">
<div class="ks-brand"><span class="ks-logo" aria-hidden="true">L</span>Ledgerline</div>
<nav class="ks-nav" aria-label="Main">
<a href="#overview"><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>Overview</a>
<a href="#invoices" 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"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg>Invoices</a>
<a href="#vendors"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21h18"/><path d="M5 21V8l7-5 7 5v13"/><path d="M9 21v-6h6v6"/></svg>Vendors</a>
<a href="#payments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="5" width="19" height="14" rx="2"/><path d="M2.5 10h19"/><path d="M6.5 15h3"/></svg>Payments</a>
<a href="#reports"><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 20V10"/><path d="M10 20V4"/><path d="M16 20v-7"/><path d="M22 20H2"/></svg>Reports</a>
<a href="#settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg>Settings</a>
</nav>
<p class="ks-side-foot">Northwind Trading LLC<br>Fiscal year 2026</p>
</aside>
<section class="ks-main" aria-labelledby="ks-title">
<div class="ks-head">
<div>
<h3 id="ks-title">Invoices</h3>
<p>Q3 2026 · 128 open · $412,380.00 due</p>
</div>
<div class="ks-actions">
<button class="mv-button" type="button" data-variant="outline" data-size="sm" data-shortcut="Mod+Shift+E" data-log="Exported 128 invoices to CSV">Export CSV</button>
<button class="mv-button" type="button" data-size="sm" data-shortcut="g n" data-log="New invoice draft opened">New invoice</button>
</div>
</div>
<div class="ks-tools">
<input class="mv-input" type="search" data-size="sm" placeholder="Search vendor or invoice #" aria-label="Search invoices" aria-keyshortcuts="/">
<select class="mv-select" data-size="sm" aria-label="Due date">
<option>Due this month</option>
<option>Due next 30 days</option>
<option>Past due</option>
</select>
<nav class="ks-filters" aria-label="Status">
<a href="#all" aria-current="page">All <small>128</small></a>
<a href="#pending">Pending <small>42</small></a>
<a href="#overdue">Overdue <small>7</small></a>
<a href="#paid">Paid</a>
</nav>
</div>
<div class="ks-table-wrap">
<table class="ks-table">
<thead>
<tr>
<th class="ks-check"><input type="checkbox" class="mv-checkbox" aria-label="Select all invoices"></th>
<th>Invoice</th>
<th>Vendor</th>
<th class="ks-num">Amount</th>
<th class="ks-hide-sm">Due</th>
<th>Status</th>
<th><span class="mv-sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="ks-rows">
<tr>
<td><input type="checkbox" class="mv-checkbox" aria-label="Select INV-2041"></td>
<td><a class="ks-link" href="#inv-2041">INV-2041</a></td>
<td class="ks-vendor">Kasai Logistics</td>
<td class="ks-num">$18,420.00</td>
<td class="ks-hide-sm">Oct 3, 2026</td>
<td><span class="mv-badge" data-variant="warning">Pending</span></td>
<td><div class="ks-row-actions"><button class="mv-button" type="button" data-variant="ghost" data-size="sm" data-approve>Approve</button><button class="mv-button ks-del" type="button" data-variant="destructive" data-size="icon" aria-label="Delete INV-2041" data-delete><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></button></div></td>
</tr>
<tr>
<td><input type="checkbox" class="mv-checkbox" aria-label="Select INV-2038"></td>
<td><a class="ks-link" href="#inv-2038">INV-2038</a></td>
<td class="ks-vendor">Oliveira & Filhos</td>
<td class="ks-num">$6,975.50</td>
<td class="ks-hide-sm">Sep 28, 2026</td>
<td><span class="mv-badge" data-variant="danger">Overdue</span></td>
<td><div class="ks-row-actions"><button class="mv-button" type="button" data-variant="ghost" data-size="sm" data-approve>Approve</button><button class="mv-button ks-del" type="button" data-variant="destructive" data-size="icon" aria-label="Delete INV-2038" data-delete><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></button></div></td>
</tr>
<tr>
<td><input type="checkbox" class="mv-checkbox" aria-label="Select INV-2035"></td>
<td><a class="ks-link" href="#inv-2035">INV-2035</a></td>
<td class="ks-vendor">Nordlicht GmbH</td>
<td class="ks-num">$42,300.00</td>
<td class="ks-hide-sm">Oct 12, 2026</td>
<td><span class="mv-badge" data-variant="warning">Pending</span></td>
<td><div class="ks-row-actions"><button class="mv-button" type="button" data-variant="ghost" data-size="sm" data-approve>Approve</button><button class="mv-button ks-del" type="button" data-variant="destructive" data-size="icon" aria-label="Delete INV-2035" data-delete><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></button></div></td>
</tr>
<tr>
<td><input type="checkbox" class="mv-checkbox" aria-label="Select INV-2029"></td>
<td><a class="ks-link" href="#inv-2029">INV-2029</a></td>
<td class="ks-vendor">Adebayo Print Co.</td>
<td class="ks-num">$2,180.75</td>
<td class="ks-hide-sm">Oct 20, 2026</td>
<td><span class="mv-badge" data-variant="success">Approved</span></td>
<td><div class="ks-row-actions"><button class="mv-button" type="button" data-variant="ghost" data-size="sm" data-approve disabled>Approve</button><button class="mv-button ks-del" type="button" data-variant="destructive" data-size="icon" aria-label="Delete INV-2029" data-delete><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></button></div></td>
</tr>
<tr>
<td><input type="checkbox" class="mv-checkbox" aria-label="Select INV-2024"></td>
<td><a class="ks-link" href="#inv-2024">INV-2024</a></td>
<td class="ks-vendor">Tanaka Components</td>
<td class="ks-num">$11,040.00</td>
<td class="ks-hide-sm">Nov 2, 2026</td>
<td><span class="mv-badge" data-variant="warning">Pending</span></td>
<td><div class="ks-row-actions"><button class="mv-button" type="button" data-variant="ghost" data-size="sm" data-approve>Approve</button><button class="mv-button ks-del" type="button" data-variant="destructive" data-size="icon" aria-label="Delete INV-2024" data-delete><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></button></div></td>
</tr>
</tbody>
</table>
</div>
<div class="ks-foot">
<span>Showing 1-5 of 128</span>
<nav class="ks-pager" aria-label="Pagination">
<a href="#prev" aria-disabled="true">Previous</a>
<a href="#next">Next</a>
</nav>
</div>
</section>
</div>
</mv-keen-sight>
<p class="ks-log" id="ks-log">Type a hint shown on the app: <b>F</b> goes to Overview. <kbd class="mv-kbd">Shift</kbd> on the last letter only moves focus.</p>
<script type="module">
const ks = document.getElementById("ks-app");
await customElements.whenDefined("mv-keen-sight");
const log = document.getElementById("ks-log");
const say = (...parts) => {
log.replaceChildren(...parts.map((p) => (typeof p === "string" ? p : Object.assign(document.createElement("b"), { textContent: p.b }))));
};
if (/mac|iphone|ipad/i.test(navigator.userAgentData?.platform || navigator.platform || "")) document.getElementById("ks-key").textContent = "⌥ Option";
const nameOf = (el) => el.getAttribute("aria-label") || el.textContent.replace(/\s+/g, " ").trim();
ks.addEventListener("mv-hint-activate", (e) => {
const { target, hint, action, shortcut } = e.detail;
const via = hint ? `hint ${hint.toUpperCase()}` : `shortcut ${shortcut}`;
say(action === "focus" ? "Focused " : "Activated ", { b: nameOf(target) }, ` · ${via}`);
});
// The app itself: links stay on the page, rows can be approved or deleted.
ks.addEventListener("click", (e) => {
const link = e.target.closest("a[href^='#']");
if (link) {
e.preventDefault();
const group = link.closest(".ks-nav, .ks-filters");
if (group) for (const a of group.querySelectorAll("a")) {
if (a === link) a.setAttribute("aria-current", "page");
else a.removeAttribute("aria-current");
}
return;
}
const btn = e.target.closest("button");
if (!btn) return;
const row = btn.closest("tr");
if (btn.hasAttribute("data-approve")) {
const badge = row.querySelector(".mv-badge");
badge.dataset.variant = "success";
badge.textContent = "Approved";
btn.disabled = true;
} else if (btn.hasAttribute("data-delete")) {
row.setAttribute("data-removing", "");
setTimeout(() => row.remove(), 260);
} else if (btn.dataset.log) {
say({ b: btn.dataset.log });
}
});
document.getElementById("ks-shortcuts").addEventListener("change", (e) => { ks.shortcuts = e.target.value; });
document.getElementById("ks-sticky").addEventListener("change", (e) => { ks.sticky = e.target.checked; });
</script>
</div>Referência cultural
Assassin's Creed, Ubisoft Montreal (2007, jogo). Segurar um botão ativa um sentido aguçado em que o mundo fica cinza enquanto cada alvo, aliado e objeto utilizável se destaca na sua própria cor, então o jogador vê de relance sobre o que pode agir. Na interface, segurar uma tecla deixa a página em cinza e recorta cada elemento acionável, com cor e forma conforme o que ele faz, cada um com uma dica curta que o ativa quando digitada.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
key | Alt | Control | Meta | Mod | none | Alt | Key to hold. It must be pressed alone (no other modifier) and held for hold-delay: any other key first means a chord (Alt+Tab, Alt+F…) and cancels. AltGr is never taken. With Control, Meta or Mod (⌘ on Apple, Ctrl elsewhere) the mode stays open after release, because Ctrl/⌘ + letter belongs to the browser. none: no hold trigger (toggle-key, toggle buttons and show() only). |
hold-delay | time (ms number, "250ms", "0.3s") | 250 | How long the key must be held alone before the hints appear. |
toggle-key | chord ("Mod+Shift+H", "F6") | Chord that opens the mode latched (no holding, for users who cannot hold keys). Must contain Ctrl, Alt, ⌘/Meta or Mod, or be a function key: a bare character key is refused. Letters also match by physical key. | |
sticky | boolean | Releasing the held key keeps the hints open; a hint, Escape, a click or another key closes them. | |
open | boolean | Reflects whether the mode is open (set by the component). Setting it opens the mode latched, as show() does; removing it closes. | |
global | boolean | Scans the whole document and veils the whole viewport instead of the element's children. A scoped instance takes the held key when focus or the pointer is inside it; a global one otherwise. | |
include | CSS selector | Extra targets on top of the built-in list (a[href], buttons, fields, summary, contenteditable, [role=button|link|tab|checkbox|radio|switch|menuitem*|option|treeitem|combobox|textbox|searchbox|slider|spinbutton], [data-keen-sight-kind], [data-keen-sight-hint]). | |
exclude | CSS selector | Targets to skip. data-keen-sight="off" on any element also skips it and everything inside. | |
alphabet | string | fjdkslaghrueiwoqpcmvnxbzty | Letters used for hints, most comfortable first (home row). The first targets in reading order get single letters; the last letters become prefixes of two-letter hints only when needed (up to alphabet² targets). |
shortcuts | replace | both | ignore | replace | Declared shortcuts (aria-keyshortcuts, data-shortcut, data-combo): replace shows the shortcut instead of a hint (the element then has no hint), both shows the hint followed by the shortcut, ignore hides shortcuts. |
in-fields | boolean | Allows the hold trigger while typing in a text field. Off by default: Option + letter types accented characters on a Mac and Alt + arrows moves by word. | |
platform | auto | mac | windows | linux | auto | How shortcuts and the held key are displayed (⌘⇧E vs Ctrl+Shift+E). Display only. |
show-status | "true" | "false" | true | "false" hides the status pill at the bottom of the region (typed letters, count, legend of shapes, how to exit). |
data-keen-sight-kind | navigate | act | input | destructive (on a target) | Overrides the detected kind (and makes the element a target). Detection: links and tabs navigate, text-like fields, selects, contenteditable and field roles are inputs, data-variant="destructive|danger", data-destructive and data-danger are destructive, everything else acts. | |
data-keen-sight-hint | 1-2 letters (on a target) | Pins a mnemonic hint (e.g. "n" for New). Pinned hints are reserved before generating the others, which never collide with them. | |
data-keen-sight-action | click | focus (on a target) | What typing the hint does: click (focus, then click) or focus only. Default: focus for inputs and sliders, click otherwise. | |
data-keen-sight-toggle | id of the instance, or empty (on any button) | Makes a button open or close the mode latched; it gets aria-pressed. Empty value: the instance containing it (or the global one). | |
aria-keyshortcuts / data-shortcut / data-combo | on a target | Declared shortcut shown in its badge. aria-keyshortcuts: first alternative (space-separated). data-shortcut and data-combo: first alternative (comma-separated), steps separated by spaces ("g n"), Mod = ⌘ on Apple, Ctrl elsewhere. |
Properties
| Name | Tipo | Description |
|---|---|---|
targets | Array<{ element, hint, kind, action, shortcut }> | Targets currently labeled, in reading order (empty while closed). hint is null for elements that show their own shortcut. |
typed | string | Letters typed so far in the current hint (read-only). |
strings | Partial<Record<string, string>> | Overrides for visible texts and announcements (keys: announceOpen, announceOpenHeld, announceNone, announceNarrow, announceNoMatch, announceShortcuts, target, targets, prompt, navigate, act, input, destructive, exit, exitHeld; placeholders {count}, {key}, {typed}). English defaults. |
key / holdDelay / toggleKey / sticky / open / global / include / exclude / alphabet / shortcuts / inFields / platform / showStatus | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
show({ source? }) | Opens the mode latched (source is reported in mv-hint-open, default "api"). Returns false if mv-hint-open was canceled or the element is not connected. |
close(reason?) | Closes the mode; reason (default "api") is reported in mv-hint-close. |
toggle() | Opens or closes; returns whether it is now open. |
activate(hint | element, { focusOnly? }) | Activates a labeled target as if its hint had been typed (mv-hint-activate is emitted, with source "api"). Returns false when the mode is closed or the target is not labeled. |
refresh() | Re-scans now, after a layout change the component cannot observe (a canvas redraw, an animation that ended). |
Events
| Name | Description |
|---|---|
mv-hint-open | Cancelable, before the hints appear. detail: { source: "hold" | "toggle" | "api", count, targets }. preventDefault() keeps the mode closed (e.g. while a modal flow is running). |
mv-hint-activate | Cancelable, when a hint is completed or activate() is called, before anything happens. detail: { target, hint, kind, shortcut, action: "click" | "focus", source: "hint" | "api" }. The mode closes either way; preventDefault() keeps the element from being focused and clicked, so the app can route it itself. |
mv-hint-close | The mode closed. detail: { reason: "release" | "escape" | "activate" | "pointer" | "key" | "toggle" | "blur" | "api" | "disconnect" }. |
Content structure
| Name | Description |
|---|---|
(content) | Any region with links, buttons and fields; left untouched (the overlay is appended to the element and lives in the top layer while open). Empty with global. |
CSS classes
| Name | Description |
|---|---|
mv-keen-sight-layer | Overlay (aria-hidden, pointer-events: none, popover="manual" in the top layer): -veil (masked backdrop), -field (marks), -status. data-held while the key is held. |
mv-keen-sight-mark | One per target, laid over it: data-kind, data-out (does not match what was typed), data-next (still matches); its ::before is the outline (solid, dashed for inputs, double for destructive). |
mv-keen-sight-badge / -shape / -char / -keys / -sep | Hint badge on the target's corner: shape by data-kind, one -char per letter (data-typed once typed), -keys for a declared shortcut (data-shortcut on the badge when it replaces the hint). |
mv-keen-sight-status / -typed / -slot / -count / -legend / -key / -swatch / -exit | Status pill: typed letters in slots, target count, legend of the shapes present, exit key. data-reject shakes it when a letter matches nothing. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-keen-sight-navigate | var(--mv-info) | Color of links and tabs. |
--mv-keen-sight-act | var(--mv-accent) | Color of buttons and other actions. |
--mv-keen-sight-input | var(--mv-success) | Color of fields. |
--mv-keen-sight-destructive | var(--mv-danger) | Color of destructive controls. |
--mv-keen-sight-veil | color-mix(in oklab, var(--mv-bg) 52%, transparent) | Tint of the veil over everything that is not a target. |
--mv-keen-sight-filter | grayscale(1) contrast(0.9) | backdrop-filter of the veil (none to only dim). |
Accessibility
Additive and opt-in: the overlay is aria-hidden with pointer-events: none, the app's DOM is never rewritten, and once activated a target is focused (with a visible focus ring) before being clicked, so screen readers read where the user landed and fields enter focus mode. Opening is announced once in a polite live region with the count (“Hint mode: 24 targets, type a hint. Escape to exit.”, plus how many show their own shortcut); narrowing (“2 match.”) and refused letters (“No hint starts with Q.”) are announced politely too. Screen readers: the hold trigger is Alt/Option pressed alone, which no major screen reader uses as its own modifier (NVDA and JAWS use Insert/Caps Lock, Narrator Caps Lock/Insert, VoiceOver Control+Option); any other modifier pressed before the delay cancels the hold, and one pressed while the mode is open (a Control+Option VoiceOver command) closes it and passes through untouched. In browse mode letters never reach the page, so nothing is hijacked; teams whose users rely on Alt can set key="none" and keep toggle-key or a toggle button. Nothing is taken while the mode is closed; while open, only hint letters, Backspace, Shift and Escape are consumed, and any other key (Tab, arrows, Enter, chords) closes the mode and does its normal job. Holding a key is never required (WCAG 2.1.1): toggle-key, sticky and [data-keen-sight-toggle] buttons (aria-pressed) open it latched, and there is no time limit to type a hint (WCAG 2.2.1). The hold trigger is ignored in text fields unless in-fields is set. Kinds are told apart by shape and outline style, not only color (tag, block, underlined field, octagon; solid, dashed, double). Reduced motion (OS or data-motion="reduce"): no fade, no staggered reveal, no shake. Forced colors: the veil is removed and outlines and badges use system colors (Highlight, LinkText, CanvasText, Mark), shapes unchanged. Performance: while closed only key listeners exist; while open, one read pass (rects, one hit test per candidate) then one write pass per frame at most, on scroll, resize or DOM changes.