Feature hints (in-app guidance)¶
A feature hint is a one-time bubble anchored to a button, menu item or any other element that introduces a new
(or under-used) feature. It shows an optional badge (typically New), a title, a short text, an optional image or
short video loop and two actions: Got it and an optional Learn more link to documentation. Every user sees a given hint at most once.
When to use¶
- Announcing a feature that is new for existing users and lives behind a concrete UI element.
- Nudging adoption of an existing feature (set
released_onso brand-new users are not bothered).
Do not use hints for errors, validation, required actions or anything that must not be missed; every user can switch them off in their preferences.
Rails usage¶
Put the call next to the element it points to, inside the owning plugin's view or hook partial. The element carries
data-hint-anchor with the hint key; that is how the bubble finds it:
<%= ds_button(text: l(:button_ask_aura), data: { hint_anchor: "hint.easy_ai.aura_button" }) %>
<%= feature_hint(
key: "hint.easy_ai.aura_button",
title: l(:hint_aura_title),
text: l(:hint_aura_text),
media: image_path("hints/aura.webp"),
learn_more_url: "https://www.easy8.com/docs/aura",
placement: :bottom_end,
badge: { text: l(:label_new) },
released_on: "2026-08-26"
) %>
The helper renders nothing when the user already dismissed the hint, when hints are disabled for them, or when the
release is not relevant for them (see below). Otherwise it emits an easy8-feature-hint custom element.
| Parameter | Meaning |
|---|---|
key |
Unique, stable key namespaced with hint. (hint.<plugin>.<feature>). The anchored element carries it in data-hint-anchor. Renaming resets the hint. |
title, text |
Localized copy. Keep the text to one or two sentences. |
media |
Image URL or a short mp4/webm loop (muted, autoplay; paused under prefers-reduced-motion). |
learn_more_url |
Documentation link; renders the Learn more action (opens in a new tab and dismisses). |
placement |
:bottom_start (default), :bottom_end, :top_start, :top_end, :left_start, :left_end, :right_start, :right_end; the kebab-case string form ("bottom-start") is accepted too; an unknown value falls back to bottom-start. The arrow sits on the opposite card edge and is aligned with the anchor's center at runtime. The card is not kept inside the viewport: it scrolls out of view together with the anchor, so pick a placement that fits where the anchor sits. |
released_on |
Release date. Users created after it never see the hint; the hint expires 90 days after it. |
priority |
When a page contains several hints, the highest priority is shown first. |
badge |
DS badge props hash (text, icon, state, size); no badge when omitted. |
spotlight |
Pass true to dim the page with the DS backdrop overlay (Background/Backdrop); the anchor stays visible in a rounded cut-out and clickable. Clicking the backdrop closes the hint. |
Vue usage¶
<script setup lang="ts">
import { FeatureHint } from "@/src/shared_modules/feature_hints";
</script>
<template>
<DSButton data-hint-anchor="hint.easy_ai.aura_button">Ask Aura</DSButton>
<FeatureHint
hint-key="hint.easy_ai.aura_button"
:title="t('hint_aura_title')"
:text="t('hint_aura_text')"
:badge="{ text: t('label_new') }"
learn-more-url="https://www.easy8.com/docs/aura"
/>
</template>
FeatureHint (shared module) wraps the presentational DSFeatureHint card and adds everything stateful: it reads the
dismissal state, registers with the page coordinator, persists the dismissal and sends UTM tracking. Use the bare
DSFeatureHint only when you manage open and persistence yourself (Storybook, guided flows). The badge renders only
when badge is given.
How it behaves¶
- Once per user. Dismissals are stored per user in
in_app_news_dismissalsunder thehint.key prefix (FeatureHintsineasy_engines/easy_extensions/lib/feature_hints.rb) and persisted through thedismissInAppNewsGraphQL mutation.Got it,Learn more,Escape, clicking the anchored element and clicking the spotlight backdrop all count as dismissal. There is no separate close button; it would duplicateGot it. - Bootstrap without extra requests. The layout exposes
window.EASY.featureHints = { enabled, dismissed }; both the Rails helper and the Vue wrapper read it, so a page with hints costs one indexed query. - Show budget. The coordinator (
shared_modules/feature_hints/utils/featureHintsCoordinator.ts) opens at most one hint per context and at most one per calendar day per browser. A context is the page load, or each<dialog>while it is open: hints rendered inside a modal (for example inLogTimeModal) show once the modal has settled, page hints never show over an open modal and an open page hint is hidden (not dismissed) the moment a modal opens. Only native<dialog>elements (DSDialog) form a context; legacy jQuery UI modals (showModal,.ui-dialog) are not detected, so a page hint can open over them and a hint anchored inside one behaves as a page hint. The bubble appears about 800 ms after the context settles; an anchor that renders later (lazy dashboard modules, async forms) is awaited for up to 15 s. The budget is spent only when a hint actually opens: a hint whose anchor never renders (a dashboard module the user does not have, a button hidden by permissions) leaves the day for another hint. - Focus. The card takes keyboard focus so
Escapeand the actions are reachable, except when the user is typing in a field at that moment; then the focus stays where it is. - Gating. Each user can opt out with Show tips about new features in their preferences. Anonymous users and
external clients never see hints.
EasySetting :feature_hints_enabled(defaulttrue) switches hints off for the whole instance; it has no administration UI yet, so change it from the Rails console:
setting = EasySetting.find_or_initialize_by(name: "feature_hints_enabled", project_id: nil)
setting.update!(value: false)
- Tracking. Dismissals go out with
utm_campaign=feature_hints,utm_content=<key>andutm_term=<reason>(got_it,learn_more,close,anchor). The campaign must be present in the EWA export whitelist (internal/logs,app/jobs/exports/ewa_feature_one_job.rb), otherwise usage is not exported as a feature metric.
Testing and support¶
- Forget a user's dismissed hints so they show again from the Rails console (replace
loginwith the user's login):
- The daily budget lives in
localStorageundereasy8.featureHints.lastShownOn; remove it to see another hint today. - Append
?feature_hints_preview=1to any URL to walk through every hint on that page one by one (the next opens when you close the current one), ignoring the daily budget and the one-per-context rule. Closing a hint in preview does not persist anything, so the walkthrough can be repeated. - Hints are disabled when the
PLAYWRIGHTenvironment variable is set and in the Railstestenvironment, so Playwright, VRT and system specs never get a hint card over the UI. Development and production render hints (unlike the What's new modal, there is no production-only gate). A system spec that needs a hint stubs the gate withallow(Rails.env).to receive(:test?).and_return(false).