Components · Overlays

Coach marks

A small, dark popover that points at one element on the page and explains why it matters. The interface's only sanctioned 'over the shoulder' moment — used to introduce a new control, to mark the next step in a workflow, never to remind the user of something they already know.

Documentedby Derek Fidler
Burger1 of 4
Patty sizeRequired
Choose 1
TemperatureRequired
Choose 1
Toppings
Choose 1

Overview

A coach mark is a 280 px popover anchored to a target element. The surface is the inverse accent — pure black — so it reads as a deliberate interruption of the ordinary page chrome. One title, one short body, an optional preview, and an optional two-button action row. Nothing else.

Coach marks teach. Tooltips remind. Banners announce.

If the user has seen the control before, use a tooltip on hover. If the message is for everyone on this surface (incident, outage, policy change), use a banner. The coach mark is for moments that need to land once — a new feature, the next step of an onboarding flow, a setting whose effect lives on a different screen.

Anatomy

Five parts. Title, body, optional preview, optional action row, and the arrow that anchors the whole thing to a target.

  1. Arrow

    12 × 12 diamond on the popover's top edge — same fill as the surface, background.accent.inverse.subtlest. Three positions: right, left, center.

  2. Title

    Inter Tight Semibold 16 / 24 in text.inverse. Sentence case. One line — wraps to two only when the language requires it.

  3. Body

    Inter Tight Regular 14 / 20 in text.inverse at 92% opacity (lighter type on dark surfaces wants more air). Cap at three lines so the popover never grows past ~220 px tall without a preview.

  4. Preview (optional)

    248 × 200 image area on background.accent.neutral.subtler with a 1 px border.accent.inverse hairline. Only when the result of the action lives on a different screen.

  5. Actions (optional)

    Two buttons, equal-width. Secondary (transparent + white border + white text) on the left, primary (white surface + black text) on the right. Used for multi-step tours; not for single-shot coach marks.

Arrow positions

The popover always sits below its target. Three horizontal arrow positions place the tip directly above the element being described, regardless of where the popover itself lands. Pick the position that puts the arrow under the visual centre of the target.

right

Arrow sits 16 px in from the right edge. Use when the target sits at the right side of a row — column headers, end-of-row controls.

left

Arrow sits 16 px in from the left edge. Use when the target sits at the left — list rows, primary navigation items.

center

Arrow centers under the popover. Use when the target sits roughly in the middle — page-level toolbars, centred CTA bars.

Content variants

Title + body is the floor. Preview and actions earn their place one at a time, never reflexively.

Title and body

The minimum. One sentence to point to the target, one sentence to explain. No preview when the target is already visible behind the popover.

+ preview image

Use a preview when the result of the action sits on a different screen — onboarding, feature announcements, settings pages with downstream effects the user can't see.

+ actions

Two buttons — secondary on the left (outline on inverse), primary on the right (white on inverse). Use the action row only when the coach mark is part of a multi-step tour.

Full

Title + body + preview + actions. Use sparingly — full coach marks dominate the screen and lose impact when stacked.

Multi-step tour

A guided tour stitches a sequence of coach marks together — each one anchored to the next target. The action row carries Back / Next; the secondary button on step one becomes Skip. A small dash progress indicator sits below the row so the user can see how many steps are left.

Burger1 of 4
Patty sizeRequired
Choose 1
TemperatureRequired
Choose 1
Toppings
Choose 1

Live demo — click Next / Back to step through.

Behavior

  • Click outside dismisses. Anywhere outside the popover closes it. The interaction is non-modal — the user can keep working with the page underneath. This is the default reflex for coach marks; if you need to block the user, you wanted a Modal.
  • Once-per-user, by default. Persist a flag against the user account when the coach mark is dismissed or its primary action is taken. Don't show it again on the next visit; that's how an introduction becomes a nag.
  • One coach mark on screen at a time. Multiple coach marks at once is a UI emergency — the user has no idea where to look. In a tour, the previous mark must animate out before the next one animates in.
  • Anchor with a 16 px gap. The arrow tip sits 16 px below the bottom edge of the target. Closer than that the arrow looks pasted on; further away the user has to track to find what the popover is pointing at.
  • Reposition before flipping. When the popover would clip the viewport, shift it horizontally first (and update the arrow position to match). Only flip to above the target if there's no horizontal room either.
  • Reduced motion respected. The default entrance is a 120 ms 4 px rise + opacity. Under prefers-reduced-motion: reduce the popover appears in place with opacity only.

Accessibility

  • Roles: the popover is role="dialog" with aria-modal="false" (the user is not blocked) and aria-labelledby pointing at the title element.
  • Focus management:open the popover and move focus to the primary action (or the close button if there are no actions). Trap focus inside the popover until it's dismissed; restore focus to the target on close.
  • Keyboard: Escape dismisses; Tab / Shift+Tab cycles through the actions; in a tour, advances and goes back when no action button has focus.
  • Contrast: the dark surface gives text.inverse on background.accent.inverse.subtlest an effective contrast of 21 : 1 — well past the AA requirement. The body text runs at 92% opacity (~16.5 : 1) so it still clears AA at small sizes.
  • Announcement: on open, an off-screen aria-live="polite" region announces “Tip: {title}” so screen-reader users know a coach mark has appeared without losing their place.
  • Touch targets: the action buttons are 48 px tall — the platform tap-target floor. The arrow is decorative and not focusable.

Code

The component composes from a single CoachMark primitive and a positioning hook (useCoachMark) that handles anchoring, dismissal, and the once-per-user flag.

tsx

import { CoachMark, useCoachMark } from "@flatpay-dk/ui";

// Single-shot — anchors to the ref, persists dismissal under "required-questions"
const { ref, open, dismiss } = useCoachMark({
  id: "required-questions",
  showOnce: true,
});

return (
  <>
    <button ref={ref}>Required</button>
    {open ? (
      <CoachMark
        position="right"
        title="Required questions"
        body="Add a Required tag to a question to block the order until the customer answers."
        primaryLabel="Got it"
        secondaryLabel="Skip"
        onPrimary={dismiss}
        onSecondary={dismiss}
      />
    ) : null}
  </>
);

// Multi-step tour
const tour = useCoachTour({
  id: "burger-config",
  steps: [
    { target: pattyRef, title: "Required questions", body: "..." },
    { target: tempRef,  title: "Defaults",           body: "..." },
    { target: extraRef, title: "Optional toppings",  body: "..." },
  ],
});

return (
  <CoachMark
    position={tour.position}
    title={tour.step.title}
    body={tour.step.body}
    secondaryLabel={tour.isFirst ? "Skip" : "Back"}
    primaryLabel={tour.isLast ? "Done" : "Next"}
    onSecondary={tour.back}
    onPrimary={tour.next}
  />
);

Best practices

Coach marks are easy to abuse. The questions below catch the most common misuses before they reach the screen.

Do

Use a coach mark to introduce a control whose effect lives somewhere else. The preview shows what the user can't see from here.

Don't

Don't use a coach mark for a label the user can read. Tooltip on hover is the right reach.

Do

Pin one coach mark per screen. The popover lands; the user reads it; the popover dismisses; back to work.

Don't

Don't stack two coach marks. The user has no idea which target is being described — and the second arrow lies.

Do

Persist dismissal. Once a coach mark is closed or completed, it stays closed for that user.

Don't

Don't reopen the same coach mark every visit. Repetition turns help into harassment.

Props

PropTypeDefaultDescription
titlestringInter Tight Semibold 16/24, text.inverse. Sentence case. One line preferred.
bodyReactNodeInter Tight Regular 14/20, text.inverse at 92% opacity. Cap at three lines without a preview.
previewReactNodeOptional 248 × 200 image slot — typically a screenshot mock that shows the result of the action.
primaryLabelstringRight action button. Pair with secondaryLabel — both must be set for the action row to render.
secondaryLabelstringLeft action button. Becomes 'Skip' on the first step of a tour, 'Back' on subsequent steps.
onPrimary() => voidClick handler for the primary action. Default behaviour dismisses the coach mark.
onSecondary() => voidClick handler for the secondary action. Default behaviour dismisses without persisting completion.
position"right" | "left" | "center""right"Horizontal arrow position relative to the popover. Pick the one that lands the arrow under the target.