Skip to content

Positioning guide

GlowTour.js positions the popover and pointer (indicator) around the target element and automatically handles collisions with the viewport. You control which placements to try and in what order; GlowTour.js picks the first one that fits.

Both the popover and pointer support a placementTryOrder array. Valid placements are:

  • top - Above the target
  • bottom - Below the target
  • left - To the left of the target
  • right - To the right of the target

If none of these fit within the viewport (considering the viewport-gap), the popover or pointer falls back to center.

Control where the popover tries to appear relative to the target:

const workflow = tour
.create("placement-test")
.step({
id: "feature",
target: "#feature",
title: "Where's the popover?",
content: "Try positioning it around the target.",
popover: {
placementTryOrder: ["right", "bottom", "left", "top"],
},
})
.build();

The popover will try to render to the right of the target first. If that doesn’t fit, it tries below, then left, then above. If none fit, it centers on the screen.

Control where the pointer/indicator tries to appear:

const workflow = tour
.create("pointer-placement")
.step({
id: "button",
target: "#button",
title: "Indicator position",
content: "The pointer indicates the target element.",
indicator: {
placementTryOrder: ["top", "right", "bottom", "left"],
},
})
.build();

The gap option does double duty: it is both the spacing between the popover (or pointer) and its target, and the minimum margin kept from the viewport edges. It defaults to 16 for the popover and 16 for the pointer. When measuring if a placement fits, GlowTour.js checks:

popover position + popover size + gap <= viewport edge

If a placement fails this check, the next placement in placementTryOrder is tried. If all placements fail, the popover/pointer centers on the screen.

The default theme’s --glow-tour-viewport-gap token (also 16px) caps the popover’s max-width and max-height at 100vw - 2 * gap. Keep the two in step: raising gap above the CSS token means a popover at its maximum size no longer fits anywhere, and every step falls back to centered.

If all placements fail due to viewport constraints, the popover or pointer will center itself. Lower the gap to let a step sit closer to the edges before that fallback kicks in:

const tour = createGlowTour({
popover: {
placementTryOrder: ["bottom", "top", "right", "left"],
},
indicator: {
placementTryOrder: ["top", "bottom", "right", "left"],
},
});
// Reduce the gap to allow closer positioning to edges
const workflow = tour
.create("tight-layout")
.step({
id: "corner",
target: "#corner",
title: "Tight space",
content: "In this corner, we have minimal space.",
popover: {
placementTryOrder: ["right", "bottom"],
gap: 8,
},
})
.build();

If you don’t specify a placementTryOrder, the default is:

  • Popover: ["bottom", "top", "right", "left"]
  • Pointer: ["left", "right", "top", "bottom"]

These defaults are chosen to work well in most layouts but can be overridden per step or per tour.

Here’s a tour that adapts its positioning to different UI elements:

const workflow = tour
.create("adaptive-tour")
.step({
id: "header-logo",
target: "#header-logo",
title: "Welcome",
content: "Click the logo to return home.",
popover: {
placementTryOrder: ["bottom", "right", "left"],
},
})
.step({
id: "sidebar-menu",
target: "#sidebar-menu",
title: "Navigation",
content: "The menu is always available on the left.",
popover: {
placementTryOrder: ["right", "bottom", "top"],
},
})
.step({
id: "main-content",
target: "#main-content",
title: "Your content",
content: "This is where your data lives.",
popover: {
placementTryOrder: ["top", "bottom", "left", "right"],
},
})
.build();

Each step tries different placements based on the region of the page it’s in, ensuring the popover always has good space and doesn’t obscure important content.


For a complete reference of all positioning options and their defaults, see Popover options and Indicator options in the Builder reference.