Back to Driver Js

Hints

apps/docs/src/content/guides/hints.mdx

1.8.08.0 KB
Original Source

import { HintsSample } from "../../components/HintsSample.tsx";

Tours walk users through your product step by step. Hints do the opposite: they sit quietly on the page as pulsing beacons, and the user decides which ones to open, in any order, whenever they want. There is no overlay and nothing is blocked. The page stays fully interactive.

Hints ship as their own entry, so tour-only users never load them. Import the module and its stylesheet. The stylesheet is self-contained, so you don't need driver.css unless you also use tours:

js
import { hints } from "driver.js/hints";
import "driver.js/dist/hints.css";

Each hint points at an element and describes it with the same popover you know from tours. The demo below also turns on the optional overlay, which spotlights the element while its hint is open:

<HintsSample buttonText="Show Hints" config={{ overlay: true, overlayOpacity: 0.5 }} hints={[ { element: "#hint-demo-export", id: "export", popover: { title: "Export your data", description: "Download this report as CSV or PDF.", }, }, { element: "#hint-demo-summary", id: "summary", beacon: { side: "left", align: "center" }, popover: { title: "Auto-generated summary", description: "This paragraph is written for you from the quarter's numbers.", side: "bottom", }, }, ]} client:load />

Here is the code for the example above:

js
const productHints = hints({
  overlay: true,
  overlayOpacity: 0.5,
  hints: [
    {
      element: "#export-btn",
      id: "export",
      popover: {
        title: "Export your data",
        description: "Download this report as CSV or PDF.",
      },
    },
    {
      element: "#summary",
      id: "summary",
      beacon: { side: "left", align: "center" },
      popover: {
        title: "Auto-generated summary",
        description: "This paragraph is written for you from the quarter's numbers.",
        side: "bottom",
      },
    },
  ],
});

productHints.show();

Closing vs. dismissing

The two are deliberately different:

  • Closing: clicking the beacon again, clicking anywhere outside, or pressing <kbd>Escape</kbd> closes the popover. The beacon stays, and the hint can be opened again.
  • Dismissing: clicking the Got it button removes the beacon entirely and fires onDismiss. The hint is gone for the session. Provide onButtonClick to take over the button and decide yourself, the same way onNextClick takes over a tour's next button.

Only one hint popover is open at a time; opening another swaps it.

Remembering dismissals

Driver.js keeps dismissals in memory for the session and stays out of the storage business. onDismiss with stable ids is the hook, and storage is yours:

js
const dismissed = new Set(JSON.parse(localStorage.getItem("hints") ?? "[]"));

const productHints = hints({
  hints: allHints.filter(hint => !dismissed.has(hint.id)),
  onDismiss: (element, hint) => {
    dismissed.add(hint.id);
    localStorage.setItem("hints", JSON.stringify([...dismissed]));
  },
});

productHints.show();

Beacon placement and styling

A beacon sits on one of twelve anchor points of its element's box: a side (top, right, bottom, left) plus an align (start, center, end). The default is the top-right corner. Set animate: false for a static dot; the pulse also pauses automatically for users who prefer reduced motion.

Size and colour come from CSS variables:

css
.driver-hint {
  --driver-hint-size: 32px;
  --driver-hint-color: #e11d48;
}

Dimming the page

Pass overlay: true to dim the page while a hint is open. The hint reads exactly like a tour step: the element is cut out of the dim and stays interactive, the popover anchors to the element rather than the beacon, and the beacon itself steps aside while its popover is up. Everything else, including the other beacons, sits under the overlay; clicking the dimmed page closes the hint like any outside click:

js
const productHints = hints({
  overlay: true,
  overlayColor: "#000",
  overlayOpacity: 0.5,
  hints: [...],
});

Using hints alongside a tour

Hints and tours coexist without any wiring: while a tour is running, the beacons hide and any open hint closes; when the tour ends, the beacons return on their own. A common pattern is a hint whose button launches the tour, using onButtonClick to take over the button:

js
const tour = driver({ steps: [...] });

const productHints = hints({
  hints: [
    {
      element: "#whats-new",
      id: "whats-new",
      popover: {
        title: "New dashboard",
        description: "Want a quick walkthrough?",
        buttonText: "Take the tour",
        onButtonClick: (element, hint, { hints: instance }) => {
          instance.close();
          tour.drive();
        },
      },
    },
  ],
});

Since hint popovers are regular Driver.js popovers, onPopoverRender works too when you need additional buttons or custom markup, exactly like in tours.

Options

Configuration passed to hints():

js
const productHints = hints({
  // Array of hints, documented below.
  hints: [],

  // Defaults applied to every hint's beacon; a hint's own values win.
  beacon: { side: "top", align: "end", animate: true, className: "" },

  // Text of the dismiss button. Defaults to "Got it".
  buttonText: "Got it",

  // Class and offset for the hint popovers, same meaning as in tours.
  popoverClass: "my-theme",
  popoverOffset: 10,

  // Dim the page while a hint is open. Off by default.
  overlay: false,
  overlayColor: "#000",
  overlayOpacity: 0.7,

  // Called when a hint popover is opened / a hint is dismissed.
  onOpen: (element, hint, { config, hints }) => {},
  onDismiss: (element, hint, { config, hints }) => {},

  // Runs instead of dismissing when the button is clicked, like a
  // tour's onNextClick takes over the default advance. Call
  // hints.dismiss(hint.id) yourself to also remove the hint.
  onButtonClick: (element, hint, { config, hints }) => {},
});

Each hint in the hints array:

js
const hint = {
  // Selector, element, or a function returning one. A hint whose element
  // is missing is skipped and picked up again on the next show().
  element: "#export-btn",

  // Stable identity, used by open/dismiss/restore and in the hooks.
  // Defaults to the hint's index.
  id: "export",

  // Where the beacon sits on the element's box, and how it looks.
  beacon: { side: "top", align: "end", animate: true, className: "" },

  popover: {
    title: "Export your data",
    description: "Download this report as CSV or PDF.",
    side: "bottom",
    align: "start",
    popoverClass: "my-theme",

    // The dismiss button; hide it for popovers you dismiss programmatically.
    showButton: true,
    buttonText: "Got it",

    // Overrides the instance-level onButtonClick for this hint.
    onButtonClick: (element, hint, { config, hints }) => {},

    onPopoverRender: (popover, { hint, hints }) => {},
  },

  // Hint-level hooks, taking precedence over the global ones.
  onOpen: (element, hint, opts) => {},
  onDismiss: (element, hint, opts) => {},

  // Anything you want to carry along; available wherever the hint is.
  data: {},
};

Methods on the returned instance:

js
const productHints = hints({ ... });

productHints.show();          // mount the beacons
productHints.hide();          // remove beacons and listeners; show() brings them back
productHints.open("export");  // open a hint's popover programmatically
productHints.close();         // close the open popover, keeping its beacon
productHints.dismiss("export");  // dismiss a hint, firing onDismiss
productHints.restore("export");  // bring a dismissed hint back
productHints.setHints([...]); // replace the hints; resets dismissals
productHints.getHints();      // the configured hints
productHints.getActive();     // the hint whose popover is open, if any
productHints.isVisible();     // whether the beacons are currently shown
productHints.refresh();       // reposition after layout changes