Exclusivobeta

Keen Sight <mv-keen-sight>

Modo de pistas que se revela al mantener una tecla y permite a los usuarios de teclado actuar sobre cualquier cosa que vean sin recorrerla con Tab. Mantén Alt (Option en Mac) un cuarto de segundo sin otra tecla, pulsa una combinación de activación o haz clic en un botón de activación: la región (o toda la página con global) se desatura y se oscurece bajo un velo con un hueco recortado para cada elemento accionable que esté realmente a la vista (enlaces, botones, campos, widgets ARIA, tu propio selector), cada uno con un contorno y una pista de 1-2 letras. Las pistas no comparten prefijos y las más cortas van primero (letras de la fila central para los primeros objetivos), y la forma de su insignia indica lo que va a pasar antes de escribir: una etiqueta para la navegación, un bloque para las acciones, un campo subrayado para las entradas, un octógono para los controles destructivos. Escribir una pista la enfoca y hace clic en ella (los campos solo se enfocan; Shift en la última letra solo mueve el foco), Backspace corrige, y una letra que no coincide se rechaza al instante. Los elementos que declaran su propio atajo (aria-keyshortcuts, data-shortcut o data-combo, mostrado según la plataforma como ⌘⇧E o Ctrl+Shift+E, y las secuencias como G · N) lo muestran en lugar de una pista, así que el modo también enseña los atajos reales. Al soltar la tecla, con Escape, un clic o cualquier otra tecla se sale; mientras el modo está cerrado, nunca se captura ninguna tecla de un solo carácter. Los objetivos se encuentran en una sola pasada de lectura (rectángulos más una prueba de impacto, así que se omite todo lo que queda tapado por un diálogo, una cabecera fija o el borde de un contenedor con desplazamiento) y se escriben en una sola pasada, se recalculan con el desplazamiento, el cambio de tamaño y los cambios del DOM, y las pistas se mantienen estables entre recálculos; mv-hint-open y mv-hint-activate son cancelables.

CategoríaNavegación
TipoWeb Component (<mv-keen-sight>)
Estadobeta
KitNavegación para usuarios avanzados
Keywordsexclusive, 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

Instalación

node scripts/add.mjs keen-sight --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["keen-sight"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): 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

Inicio rápido, el marcado mínimo 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>

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables 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 &amp; 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>

Referencia cultural

Assassin's Creed, Ubisoft Montreal (2007, videojuego). Mantener pulsado un botón activa un sentido agudizado en el que el mundo se vuelve gris mientras cada objetivo, aliado y objeto utilizable destaca en su propio color, así que el jugador ve de un vistazo sobre qué puede actuar. En la interfaz, mantener pulsada una tecla pone la página en gris y recorta cada elemento accionable, con un color y una forma según lo que hace, cada uno con una pista corta que lo activa al escribirla.

API

Attributes

NameTipoDefaultDescription
keyAlt | Control | Meta | Mod | noneAltKey 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-delaytime (ms number, "250ms", "0.3s")250How long the key must be held alone before the hints appear.
toggle-keychord ("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.
stickybooleanReleasing the held key keeps the hints open; a hint, Escape, a click or another key closes them.
openbooleanReflects whether the mode is open (set by the component). Setting it opens the mode latched, as show() does; removing it closes.
globalbooleanScans 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.
includeCSS selectorExtra 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]).
excludeCSS selectorTargets to skip. data-keen-sight="off" on any element also skips it and everything inside.
alphabetstringfjdkslaghrueiwoqpcmvnxbztyLetters 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).
shortcutsreplace | both | ignorereplaceDeclared 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-fieldsbooleanAllows 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.
platformauto | mac | windows | linuxautoHow 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-kindnavigate | 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-hint1-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-actionclick | 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-toggleid 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-comboon a targetDeclared 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

NameTipoDescription
targetsArray<{ element, hint, kind, action, shortcut }>Targets currently labeled, in reading order (empty while closed). hint is null for elements that show their own shortcut.
typedstringLetters typed so far in the current hint (read-only).
stringsPartial<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 / showStatusreflectedMirror the attributes.

Methods

NameDescription
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

NameDescription
mv-hint-openCancelable, 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-activateCancelable, 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-closeThe mode closed. detail: { reason: "release" | "escape" | "activate" | "pointer" | "key" | "toggle" | "blur" | "api" | "disconnect" }.

Content structure

NameDescription
(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

NameDescription
mv-keen-sight-layerOverlay (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-markOne 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 / -sepHint 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 / -exitStatus 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

NameDefaultDescription
--mv-keen-sight-navigatevar(--mv-info)Color of links and tabs.
--mv-keen-sight-actvar(--mv-accent)Color of buttons and other actions.
--mv-keen-sight-inputvar(--mv-success)Color of fields.
--mv-keen-sight-destructivevar(--mv-danger)Color of destructive controls.
--mv-keen-sight-veilcolor-mix(in oklab, var(--mv-bg) 52%, transparent)Tint of the veil over everything that is not a target.
--mv-keen-sight-filtergrayscale(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.

Esta página se tradujo con IA. Informar de un problema de traducción