WebberUI

Quest Map Section

A winding quest map section — as you scroll, a marker travels the path clearing stations, and each one it reaches pops its content open, raises a flag, and stamps itself complete.

Loading preview…
npx shadcn@latest add https://webberui.com/r/quest-map-section.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<QuestMapSection />

Installation

npx shadcn@latest add https://webberui.com/r/quest-map-section.json

Or, once registries are configured in components.json, install it as @webberui/quest-map-section.

Usage

Wrap a number of QuestNodes in a QuestMapSection; the stations automatically alternate left and right, and a single winding path strings all the nodes together. As you scroll, the marker advances along the path and the stations it reaches light up in order.

import {
  QuestMapSection,
  QuestNode,
} from "@/components/ui/quest-map-section";

<QuestMapSection className="text-indigo-500">
  <QuestNode title="Create an account" icon={<Compass className="h-4 w-4" />} flag="Start">
    Sign up in thirty seconds.
  </QuestNode>
  <QuestNode title="First deploy" icon={<Rocket className="h-4 w-4" />} flag="Cleared">
    Push the first build and a preview URL is generated instantly.
  </QuestNode>
</QuestMapSection>

The accent color is inherited through currentColor: pass any text-* in QuestMapSection's className to recolor the whole thing (path, marker, dots, flags, and stamps all follow).

Inside a scroll container

If the map sits in a nested container with overflow-y-auto, pass that container's ref to container so that scroll progress maps correctly:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
  <QuestMapSection container={scrollerRef}>{/* ...QuestNode */}</QuestMapSection>
</div>

Props

QuestMapSection

PropTypeDefaultDescription
childrenReactNodePut QuestNode children here, one per station
containerRefObject<HTMLElement | null>Ref of the nested scroll container; defaults to the window as scroll container
offset[string, string]["start 0.85", "end 0.55"]The range useScroll progress maps to, deciding when the marker starts and stops advancing
aria-labelstring"Quest map"Accessible label applied to the list container
classNamestringOuter styles; pass text-* to set the accent color

QuestNode

PropTypeDefaultDescription
titleReactNodeStation title
childrenReactNodeStation content description
iconReactNodeIcon before the title (a lucide-react icon is recommended)
flagReactNodeShort label shown on the flag raised when the station is reached
idstringAnchor id applied to the card
classNamestringClass applied to the outer element of the whole station row

How it works

  • Stations alternate left and right automatically by index, and the path is strung into a smooth S curve from the vertical control points of adjacent nodes; measurement uses real DOM coordinates, so it stays flush even when content heights differ.
  • The progress track is normalized with pathLength={1}, and strokeDashoffset shrinks from 1 to 0 with scroll so the fill stops exactly at the marker.
  • Node coordinates and sizes are watched with a ResizeObserver, so the path is recomputed automatically after fonts load or the container resizes.

Accessibility

  • When the user has "reduce motion" enabled at the system level, all stations render as already complete, the path is fully filled, the marker is hidden, and the travel and stamp animations do not play.
  • The container is a role="list" and each station a role="listitem", carrying data-complete plus "(complete) / (locked)" status text for announcement.
  • The path SVG and the travelling marker are both aria-hidden, so they do not interfere with assistive technology announcing the station content.

On this page