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
- Add a feature-discovery cue to a UI surface
All triggers
- Highlight a new feature in a Koder product
- Surface a 'New!' badge with explanation
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"witharia-modal="false"(does NOT trap focus; user can ignore). aria-labelledbypoints at title;aria-describedbypoints 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"andaria-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
specs/components/info-sprinkle.kmdspecs/patterns/callout-card.kmdspecs/auth/oauth-flow.kmd