Tooltips and key-caps
Hovering anything in Omniscio shows the app's own dark, themed bubble instead of the operating system's plain one. It waits a moment before appearing, follows a row as you scroll, and — on a control that has a keyboard shortcut — draws that shortcut's current key as a key-cap. The layer is installed per window, so it works in every panel, plugin panels included.
What it is
The tooltip layer is the thing that makes every hover description in the app look and behave the same, whichever way it was written. It is not a component you have to remember to use at each call site — it is a single listener that watches the whole window and upgrades whatever a control already says.
- What it reads. In order of precedence: a control's plain hint text; an icon-only button's accessible name (its
aria-label), which is what gives a bare icon button a description at all; the marker a hotkey button carries; and finally the standard accessibility shortcut attribute. - What it is not. It is not the keyboard-shortcut registry — it only displays keys. Rebinding still lives in keyboard-shortcuts.md.
- Rich content uses the shared component. Where a description needs markup, a multiline block or a link inside it, the app uses a portaled component that draws the identical look; a parity test keeps the two in step.
Where to find it
- Anywhere you hover — sidebar rows, toolbar and header buttons, tabs, menu items, settings rows, and panels built as plugins. Nothing has to opt in.
- Settings → Appearance — the bubble's background, border and text colours travel with your theme, so a custom theme restyles them along with everything else (see visual-themes.md).
- By keyboard — Tab to a control and its description appears, the same as hovering it.
- The small "i" icon beside a setting or a label opens a longer explanation on hover or focus — that is the same layer, drawing a multiline bubble.
- On a phone or tablet, there is no hover: the bubble never appears, and the description stays available to screen readers through the same accessible name.
How it behaves
It waits before it appears, and it is suppressed, not doubled. The bubble needs the pointer to settle on a control for a moment. The system's own tooltip never flashes alongside it: the moment the app claims a control, the real hint is stashed and blanked, and it is restored when you move away, click, or leave the window. If a re-render writes the text back mid-hover, it is blanked again — so a live-updating button cannot leak the system tooltip through.
It follows the list. Scroll while the bubble is up and it does not blink out: it tracks the row it belongs to. If a different described row scrolls under a stationary pointer, the description hands over to that row instead. If nothing described is under the pointer any more, it releases.
It stays on screen. A bubble near a window edge is repositioned so it is never cut off. Short text stays on one line; a longer hint wraps into a block instead of stretching across the screen.
The key is shown exactly once. A button that declares a hotkey gets its current key drawn as a key-cap — and if the button's own label also spells that key out, the written copy is dropped rather than shown twice. Key-caps are live and rebind-aware: change a binding under Settings → Keyboard shortcuts and the cap shows the new key immediately, on macOS in that platform's own notation.
Where it does not apply. A sandboxed plugin page gets the same styled bubble but never a key-cap (it is handed no key-cap builder), and an SVG element's own child <title> keeps its native behaviour.
For agents
The two halves
- The interceptor: native-title-interceptor.ts — a document-level
pointerover/focusinlistener, installed byinstallNativeTitleInterceptor(target, opts), idempotent per document (__amcNativeTitleInterceptorInstalled) with a deferred install whendocument.bodyis not ready (the plugin preload runs at document start). - The component: Tooltip.tsx —
@building-block group=ui-components reuse=Tooltip when="a portaled hover/focus help bubble — never a raw title attribute" contract=tooltip-adoption-contract; also InfoTooltip.tsx. 119 files import the component. - They deliberately do not share code (different runtimes — vanilla vs React); a parity test keeps their look in step, so a fix to one is not a fix to the other.
Resolvers and constants
Precedence in resolveHoverTarget: resolveTitleTarget → resolveAriaLabelTarget (selector button, [role="button"], a, non-empty aria-label, no visible text) → resolveHotkeyMarkerTarget (data-hotkey-action / data-hotkey-tasksv2, literals drift-asserted against hotkey-action-attr.ts) → resolveKeyshortcutsTarget (aria-keyshortcuts). A claim stashes and blanks the title, restoring it on leave, pointer-down and blur; a MutationObserver scoped to the hovered element re-blanks a rewritten title; scroll re-resolves rather than restoring. OPT_OUT_ATTR = 'data-native-title', SHOW_DELAY_MS = 400, MULTILINE_THRESHOLD = 60. The bubble is position: fixed, zIndex: 2147483646, pointerEvents: none, backdrop-filter: blur(16px), max-width: 20rem, role="tooltip", with literal fallbacks --tooltip-bg / --tooltip-border / --tooltip-fg / --shadow-tooltip.
Key-caps
opts.buildHotkeyHint / opts.buildKeyshortcutsHint are injected by each window; main.tsx passes the app builders, the plugin bridge (plugin-bridge-preload.ts) passes none, and a HUD that wants no caps passes { buildHotkeyHint: () => null } (recording-hud.tsx). The builder is appHotkeyBindingsForEl in app-hotkey-hint.ts. Theme mapping (tooltipBg → --tooltip-bg, tooltipBorder → --tooltip-border, tooltipText → --tooltip-fg) lives in custom-theme-service.ts.
Contract
Rules I1–I20 and their guards: tooltip-adoption-contract.md; the mechanism map is tooltips-system.md.
Related
- keyboard-shortcuts.md — every binding, and rebinding the keys a key-cap displays
- visual-themes.md — restyling the bubble's colours with a theme
- open-settings.md — reaching the Appearance and Keyboard shortcuts panels
- sidebar-collapse.md — a sidebar row whose description the layer renders
- inbox-overview.md — the icons and counts the bubbles describe
Last verified 2026-10-06