Skip to main content

What this lets you do

Count how many visitors actually reached your pricing table, your testimonials, or the bottom of a long post, and then use that count as a funnel step. Add one attribute to the element:
The scroll_to_pricing goal fires the moment any part of that section enters the viewport. No JavaScript, no deploy beyond the HTML edit.

Before you start

The Traceten snippet has to be installed on the page. See the install overview. The name follows the goal name rules: lowercase letters, digits and underscores, starting with a letter. An invalid or reserved name is ignored silently.

Properties

data-traceten-goal-* attributes work here too, on the same element:
fires scroll_to_pricing with { "variant": "b", "page_type": "landing" }. The same property rules and limits apply, including the fact that property keys and values are both stored and both read back (detail).

Once per element

A scroll goal fires at most once per element, per page load. Scroll past the section, scroll back up, scroll down to it again: one goal. That guarantee is per element, and an element is a DOM node. Navigating away and back within a single-page app usually rebuilds the section as a new node, and a new node counts again: two pageviews, two completions, which is the right answer. What cannot happen is the same node counting twice. This is deliberate and it is not just tidiness. Scroll goals are the highest-volume kind of goal, and every goal is a billable event on your plan. A goal that re-fired every time a section re-entered the viewport would charge you several times for one visitor reading one page, and would make the number meaningless as a funnel step.
The cap is per element, not per goal name. If you put the same data-traceten-scroll="scroll_to_pricing" on two elements, say a mobile variant and a desktop variant of one section, both fire on the same pageview and both are billed, even though the visitor only ever sees one of them. Put the attribute on a single element and style that one, or give the variants different names.

Single-page apps

A scroll goal fires at most once per element, for as long as the page is open. Not once per URL: once per element. Traceten remembers which elements have fired and never arms one of them a second time, so a visitor who reaches your pricing section, navigates elsewhere and comes back is one completion, not two. What a route change does is arm every goal element that has not fired yet, including the ones the new route just rendered. In practice that is the normal case: most frameworks build fresh DOM nodes for a new route, so a scroll_to_pricing section on /pricing is a different element from the one on /product and counts on its own. The one case where this is visible: if your app keeps the very same DOM node mounted across routes (a shared layout, a persistent sidebar), that node fires once and then not again, however many times the URL changes around it. A goal counts whether the visitor scrolled to the section or the router put it in front of them. Navigating to a route whose section is above the fold records the completion on arrival, with no scrolling, exactly as a fresh page load of that same URL would. The two entry paths agree by design. A pageview for the same URL changes nothing at all. If your app calls traceten("pageview") by hand on a route that did not actually change, no goal fires a second time.

Known limitations

Elements added after the page loads are picked up on the next route change, not immediately. Traceten scans for data-traceten-scroll elements when the page loads and again on each single-page-app route change. It does not watch the DOM continuously, because a document-wide mutation observer on every customer page is a cost we are not willing to put on your site for this feature. This includes the case where a route changes, paints a skeleton, and fills in the real content a second later: the scan runs against the skeleton and finds nothing, so that route’s new section is armed on the next route change rather than on this one. Anything already armed keeps working throughout, including sections that were waiting to be scrolled to. For a scroll goal injected into an already-loaded page without a route change (a lazy-loaded section, a modal, content revealed by a “load more” button), call traceten.track("your_goal_name") directly at the moment the content appears. Any visible sliver counts as “in view.” The goal fires as soon as any part of the element intersects the viewport, not when it is fully visible or centred. If you want “reached the bottom of the article,” put the attribute on a small element at the bottom rather than on the article itself. At most 200 scroll elements are armed, in document order. The cut is taken before anything else, on every scan, so an element that sits past the 200th in the document is never armed at all, for the life of the page load. It is not “skipped this time and picked up later”. A page with more than 200 scroll goals is almost always a selector mistake.

Browser support

Scroll goals use the browser’s IntersectionObserver API, which every current browser has had for years. Where it is missing, the feature does nothing: no goal, no error, no fallback. Traceten does not ship a polyfill for it, because the snippet is capped at 10KB and a polyfill would cost a meaningful share of that budget to serve a fraction of a percent of traffic. Everything else on the page, including click goals and pageviews, is unaffected.

Verify it worked

  1. Open the page with devtools, Network tab, filtered to conversions.
  2. Scroll the section into view.
  3. One POST to https://ingest.traceten.com/v1/conversions should appear, with your goal name in the body.
If nothing happens, check that the element is actually reachable by scrolling (a section inside a container with display: none never intersects), then work through when a goal is not recorded. The observer is installed only once a consent tool on the page has answered, so a scroll before that point records nothing.

What is stored, and for how long

A scroll goal is a conversion record like any other goal: retained for 365 days, plus a visitor step row retained for 730 days. See data collected for the full picture, and what happens to property keys and values before you attach anything to one.

Next

  • Funnels: use scroll_to_pricing as a step and see who continued.
  • Goals: the other three ways to fire one.