Skip to content

Guide cue (one-shot onboarding tooltip)

components specs/components/guide-cue.kmd

One-shot tooltip-style component with arrow + title + body + optional CTA, surfaces a single feature discovery, persists per-user so it never re-shows after dismissal. Modeled after MongoDB LeafyGreen GuideCue.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Guide cue

Status: v0.1.0 — Draft. Sits between info-sprinkle (lightweight inline help) and callout-card (full surface nudge) on the affordance weight spectrum.

R1 — Anatomy

  • Arrow indicator pointing at the highlighted feature.
  • Title (1 short sentence).
  • Body (1–2 sentences).
  • Dismiss (×) top-right; keyboard-accessible.
  • Optional CTA (verb, 1–3 words: Try it, Show me).

R2 — Positioning

  • Uses Floating UI semantics: choose side (top/right/bottom/left) based on available viewport space.
  • Arrow always points at the anchor element's nearest edge.
  • 8px gap between arrow tip and anchor element.

R3 — Trigger

  • mount-on-trigger: cue appears X ms after the anchor element mounts (default 800ms; configurable).
  • show(): imperative API to surface cue on demand (rare; usually mount-on-trigger).
  • Does NOT trigger on hover/focus (that's info-sprinkle's job).

R4 — Persistence

  • Each cue carries a stable cue_id.
  • On dismiss (× or CTA click) the cue NEVER re-shows for that user + cue_id pair.
  • Storage:
    • Authenticated: persisted via Koder ID profile (cross-device).
    • Unauthenticated: localStorage with key koder.guide_cue.{cue_id}=dismissed.
  • Resetting cues (admin / dev mode): koder.guide_cue.* localStorage clearance + Koder ID profile reset (consumer feature, not KDS-managed).

R5 — Accessibility

  • Container: role="dialog" with aria-modal="false" (does NOT trap focus; user can ignore).
  • aria-labelledby points at title; aria-describedby points at body.
  • Initial focus does NOT move into the cue — the anchor element keeps focus to avoid hijack.
  • Tab from anchor enters the cue (title → body → CTA → dismiss).
  • Esc dismisses (counts as a dismissal — never re-shows).

R6 — Multiple cues on a page

  • One visible cue per page at a time.
  • If multiple cues are eligible to show, queue them in mount order.
  • After a cue dismisses, wait 500ms before showing the next (prevents stacking).

R7 — Animation

  • Default: fade + 4px slide-in toward the anchor, 200ms ease-out.
  • prefers-reduced-motion: reduce: instant appear, no slide.

R8 — OUIA

Per specs/testing/ouia-test-hooks.kmd:

  • data-ouia-component-type="GuideCue"
  • data-ouia-component-id="<cue-id>"
  • data-ouia-safe="true" only when visible and animation complete.

Não-escopo

  • Tour sequencing (multi-cue narrative with Next/Previous) — separate ticket if demand surfaces.
  • Server-side analytics on cue impression / dismiss (consumer concern).

Requirements (testable)

Requirement: Mount-on-trigger delay {#req-guide-cue-trigger}

The guide cue SHALL surface itself a configurable delay after its anchor element mounts (default 800ms), SHALL NOT appear on hover or focus of the anchor, and SHALL otherwise remain hidden until either the mount-on-trigger delay elapses or the imperative show() API is invoked.

Scenario: Cue appears after the default delay

  • GIVEN a guide cue configured with the default 800ms mount delay
  • AND its anchor element has just mounted
  • WHEN 800ms elapse without any interaction
  • THEN the cue becomes visible pointing at its anchor

Scenario: Hover does not trigger the cue

  • GIVEN a guide cue whose mount delay has not yet elapsed
  • WHEN the user hovers over or focuses the anchor element
  • THEN the cue does NOT appear
  • AND it appears only once the configured delay elapses

Requirement: Dismissal persists per user and cue {#req-guide-cue-persistence}

Once dismissed via the × control, the CTA, or Esc, a guide cue SHALL NOT re-show for that same user and cue_id pair; an authenticated dismissal SHALL persist to the Koder ID profile and an unauthenticated dismissal SHALL persist to localStorage under the key koder.guide_cue.{cue_id}=dismissed.

Scenario: Unauthenticated dismissal persists in localStorage

  • GIVEN an unauthenticated user viewing a visible cue with cue_id "new-export"
  • WHEN the user clicks the × dismiss control
  • THEN localStorage contains koder.guide_cue.new-export=dismissed
  • AND the cue does not re-show on a subsequent page load with the same cue_id

Scenario: Authenticated dismissal persists cross-device

  • GIVEN an authenticated user who dismisses a cue with cue_id "new-export"
  • WHEN the same user loads the surface on a different device
  • THEN the cue with cue_id "new-export" does NOT re-show

Requirement: Non-trapping dialog accessibility {#req-guide-cue-a11y}

The guide cue container SHALL expose role="dialog" with aria-modal="false", SHALL reference its title via aria-labelledby and its body via aria-describedby, SHALL NOT move initial focus into the cue, and SHALL allow Tab from the anchor to enter the cue in the order title → body → CTA → dismiss.

Scenario: Cue does not hijack focus on appearance

  • GIVEN an anchor element that currently holds keyboard focus
  • WHEN the cue becomes visible
  • THEN the container has role="dialog" and aria-modal="false"
  • AND focus remains on the anchor element, not moved into the cue

Scenario: Esc dismisses and counts as a dismissal

  • GIVEN a visible cue with cue_id "new-export"
  • WHEN the user presses Esc
  • THEN the cue is hidden
  • AND the dismissal is persisted so the cue never re-shows for that user and cue_id

Requirement: Single visible cue with queuing {#req-guide-cue-queue}

When multiple cues are eligible on a page, the component SHALL display at most one cue at a time, SHALL queue the remaining eligible cues in mount order, and SHALL wait 500ms after a cue dismisses before showing the next queued cue.

Scenario: Second cue waits for the first to dismiss

  • GIVEN two eligible cues A and B where A mounted before B
  • WHEN the page renders both anchors and delays elapse
  • THEN only cue A is visible while cue B stays queued
  • AND cue B appears only 500ms after cue A is dismissed

Requirement: Reduced-motion honors user preference {#req-guide-cue-motion}

The guide cue SHALL animate with a fade plus 4px slide-in over 200ms ease-out by default, but SHALL appear instantly with no slide when the user agent reports prefers-reduced-motion: reduce.

Scenario: Reduced motion disables the slide

  • GIVEN a browser reporting prefers-reduced-motion: reduce
  • WHEN a guide cue becomes visible
  • THEN the cue appears instantly with no slide transform
  • AND no 200ms slide animation is applied

References