A scroll-depth funnel tells you where readers stop. It does not tell you whether they read. Here is how the number is computed, what to conclude from it, and how to get it without GA4, cookies or a consent banner.
Scroll depth is the furthest point a visitor reached on a page, as a percentage of the scrollable distance. A funnel of 25, 50, 75 and 100 percent turns that into one row per threshold: the share of pageviews that got at least that far.
IntersectionObserver. A working snippet is below.Want to see the panel on real traffic first? Open the live demo, or start on the free plan.
Strip away the dashboards and scroll depth is one division. Take how far the page has been scrolled, divide by how far it can be scrolled, and keep the largest value seen during the visit. In GhostMetrics the calculation is scrollY / (documentHeight - viewportHeight), rounded to a whole percent and capped at 100.
Three details in that formula decide how you should read the result.
It tracks the top edge of the viewport, not the bottom. A visitor on a tall phone who has scrolled to 50 percent has the top of their screen at the halfway point of the scrollable range. The bottom of their screen is further down, so they have seen more than half the document. A reader at 100 percent has reached the very bottom of the page, nothing more.
The denominator is the whole document. Footer links, a comment thread, a newsletter box and a related-posts grid all count as page height. If the last 25 percent of your page is chrome, then the end of the article sits near 75 percent and a reader who finished it never reaches 100.
It is a maximum, not a path. A visitor who scrolls to the bottom and jumps back to the top still records 100. A visitor who jumps straight to an anchor link halfway down records 50 without ever having seen the first half. Scroll depth measures reach, not reading order and not attention.
Pages that fit inside the viewport have nothing to scroll. The tracker only measures inside its scroll listener, so a page that never fires a scroll event usually records 0 percent even though the visitor saw everything. Short pages can therefore drag the site-wide funnel down; compare like with like.
The mechanics are small enough to describe completely, which matters if you are the one signing off on a privacy review. If you want the wider background on identifier-free measurement, our cookieless tracking explainer covers it.
sessionStorage are a per-tab pageview counter and any first-touch UTM, both cleared when the tab closes.pushState or popstate navigation counts as a new pageview, restarts the timer and starts the scroll maximum from zero. The previous route's depth and time are dropped at that moment, not sent, so in a single-page app only the route that is open when the tab is hidden reports scroll depth.On the dashboard side, the Scroll Depth Funnel panel counts, across all pageviews in the selected period, the percentage that reached at least 25, 50, 75 and 100 percent. The All Pages table adds an Avg Scroll column per URL. The funnel is site-wide for the period; if you want the same four rows for a single page, export the raw data (CSV from the Export data button, or JSON from the API; every plan) and group by page path.
A funnel is a shape, and shapes suggest hypotheses. None of the readings below is proof; each tells you what to open next. The best habit is to look at the drop between adjacent thresholds, not at any single bar.
| Shape of the funnel | Likely meaning | What to check next |
|---|---|---|
| Steep cliff between the start and 25% | Visitors leave above the fold: wrong intent, slow render, or an interruption | The headline against the search or referral source; real-user load timings |
| Healthy 25% and 50%, then a drop before 75% | The middle section loses people: a dense block, a table, a long code sample | What sits at the 50 to 75 percent mark; split it or lead with the answer |
| Flat through 75%, collapse at 100% | Readers finish and stop; the footer is the denominator, not a failure | Nothing. Treat 75% as the finishing line for this template |
| Almost everyone at 100% | Short pages where one flick of the thumb reaches the bottom | Page length; compare against pages of similar size only |
| High Avg Scroll, very short time on page | Skimming or jumping, not reading | Avg Time for the same URL in the All Pages table |
| Low Avg Scroll, long time on page | A tab left open, or a page that answers the question at the top | Whether the answer is near the top; whether the exit is a good one |
The last two rows are the reason scroll depth should never be read alone. Pair it with time on page (the dashboard has a distribution for it) and with entry and exit pages. A page with 40 percent average scroll and a two-minute average time on page is probably a reference page doing its job. The same scroll number with a six-second time is a page people abandon.
For editors, the highest-value use is comparative. Run the same template across ten articles and sort by Avg Scroll. The bottom three are candidates for a rewrite; the top three show you the structure that holds readers. Both are statements about your own pages relative to each other, which is the one comparison scroll depth supports well. For the broader picture of which numbers to trust, see how to measure website traffic.
Every scroll tracker has the same blind spots. Knowing them keeps you from rewriting a page that was fine.
Missing leave beacons count as zero. The depth is only reported when the tab is hidden or unloaded. If a browser kills a page before the beacon goes out, that pageview keeps its default of zero and drags the funnel down. Expect the 25 percent bar to understate reach somewhat, especially on mobile where tabs are backgrounded and discarded aggressively. The server also only attaches a leave beacon to a pageview recorded in the previous ten minutes, so a reader who keeps the tab in front for longer than that is recorded as zero too.
The beacon fires once per pageview. A reader who switches tabs after reading half the article, comes back and finishes, will have reported 50 percent when they first left. Anything scrolled after that point is not sent. Time on page has the same property.
Infinite scroll and lazy-loaded content move the goalposts. When the page grows as you scroll, the denominator grows with it, so the percentage can stall or fall while the reader keeps going. If a page loads more content on scroll, treat its percentages as unreliable and use section events instead.
Anchor links, sticky tables of contents and "jump to" buttons inflate depth. A visitor who taps "Pricing" at the bottom of your contents list records a high maximum having read nothing in between.
It tells you nothing about the person. There is no visitor ID, so you cannot ask "do returning readers scroll further?" and no per-visitor funnel across sessions exists. That is a design decision: no identifier means nothing to follow a person with.
The question most teams actually have is what they would give up by moving off Google Analytics. For scroll depth the honest answer is: not much, unless you rely on custom thresholds.
| Tool | Scroll depth | Cookies / consent | Notes |
|---|---|---|---|
| GA4 | Built-in scroll event from enhanced measurement, fired once at a single threshold; finer steps through Tag Manager or custom code | Cookies (_ga) by default; consent generally required in the EU/UK | Check current GA4 docs for the threshold and whether it can be changed. Strongest for tying engagement to Google Ads audiences |
| GhostMetrics | Maximum depth per pageview, funnel at 25/50/75/100, Avg Scroll per page | No cookies, no localStorage; honors GPC and DNT | Funnel is site-wide per period; per-page funnel from the raw export. No custom thresholds |
Custom event via IntersectionObserver | Whatever sections you mark up | Depends on the tool that receives the event | Best for "did they reach the pricing table", costs a few lines of JavaScript |
| Cloudflare Web Analytics | Not reported | Cookieless | Visits, pageviews, referrers, countries, devices and Core Web Vitals; no custom events |
| Netlify Analytics | Not possible | No cookies, no script | Built from server logs, so it has no client-side data of any kind |
GA4 is the one place where scroll tracking arrives bundled with a wider identity model. If your marketing team feeds engaged audiences back into Google Ads, that is a real reason to keep it, and nothing here replaces it. If the scroll event was the only reason you were reading GA4 reports, you can leave it behind; our GA4 alternative guide compares the broader options.
The bigger practical difference is consent. A cookie-based setup behind a banner gets its scroll data mainly from the visitors who accepted it. A cookieless funnel counts everyone who has not sent a privacy signal, so the sample is not limited to people who clicked "accept".
Percentages answer "how far". Editors usually want "which part". The tracker's custom-event API handles this, because an event carries the page path and one metadata field, section. The idea is to mark the sections you care about, observe them, and fire one event the first time each enters the viewport. Our guide to custom events without Google Analytics covers the API in more depth.
<script defer> tag with your site ID in the <head>. No plugin, no build step.data-section attribute: the heading that opens the pricing table, the code sample, the conclusion.</body>. It fires seen-<section> once per section per pageview.<h2 data-section="pricing">Pricing</h2>
<h2 data-section="conclusion">Conclusion</h2>
<script>
document.addEventListener('DOMContentLoaded', function () {
// gm.js is deferred, so it has run by now. It is absent if the
// visitor sends Global Privacy Control or Do Not Track.
if (!window.ghostmetrics || !('IntersectionObserver' in window)) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
var id = entry.target.getAttribute('data-section');
window.ghostmetrics.track('seen-' + id, { section: id });
io.unobserve(entry.target);
});
}, { threshold: 0.5 });
document.querySelectorAll('[data-section]').forEach(function (el) {
io.observe(el);
});
});
</script>
Putting the section into the event name is deliberate. The Custom Events panel lists name, count and last fired, with no drill-down by metadata, so seen-pricing and seen-conclusion show up as separate rows you can read at a glance. The section field still travels with each event and lands in the raw export.
Two limits to keep in mind. The panel is site-wide, so two pages that both mark a pricing section share one row; use distinct names per page type or split the export by page path. And there is no numeric value field and no per-visitor funnel, so you can count how many pageviews reached a section but not follow one reader from section to section.
Scroll data earns its keep when it changes what you write, not when it sits in a dashboard. A routine that works for most small teams:
The discipline is changing one thing at a time. Scroll depth is an average over mixed audiences, and a rewrite that coincides with a new traffic source will move the number for reasons that have nothing to do with the rewrite. The referrers panel and the UTM breakdown will tell you if the audience changed under you.
All of the numbers above come from raw rows you can export, on the free plan as on Pro, so a skeptical colleague can recompute any figure from the data rather than taking the dashboard's word for it.
GhostMetrics records scroll depth, time on page and bounce on every pageview, and shows them as a funnel, a per-page Avg Scroll column and a time-on-page distribution. Install one script tag and you are live; the free plan is free forever: one website, unlimited pageviews, real-time dashboard, every view, full history.
Need more than one site or a public shareable dashboard? Pro is $9.99/mo flat, unlimited pageviews, unlimited websites (fair-use ceiling of 250), with a 30-day free trial: a card starts the trial, nothing is charged for 30 days, and you can cancel anytime. Raw event export as CSV or JSON is on every plan, including free.
Start free, no card See the live demoDisclosure: GhostMetrics is built by Null Agency and we use it on this site, so we're not a neutral party about our own product. The product claims here describe what the tracker and dashboard do today; for GA4 and other third-party tools, check the vendor's current documentation before deciding.