If you publish stories long enough that “they opened the page” and “they read it” are very different events, you need a measurement that sits between pageviews and completion. That is what scroll depth gives you, and GA4 can collect it once you connect it to a scroll trigger in Google Tag Manager or push your own events from a small script. This guide walks through the whole setup, the verification step most people skip, and the long-form pitfalls that quietly distort the numbers.
Scroll depth is the percentage of a page’s total height a reader reached, reported as the share of readers who hit each milestone (25%, 50%, 75%, 100%). The reason it earns its own section on a newsroom dashboard: on a long story, pageviews and time on page cannot tell you whether the third section, the chart, or the conclusion is ever seen by anyone.
The whole job takes about half an hour if your tags are already in place, most of it in Google Tag Manager rather than GA4.
What You Need

Three prerequisites, and one decision to make before you touch a tag.
- A GA4 property with your site streaming into it. If the stream is not there, stop and fix that first; every scroll event you push will otherwise land in an empty property.
- Access to your tag setup through Google Tag Manager, or the ability to edit your site template if you would rather run JavaScript directly.
- A defined set of long-story pages. Decide which stories count before you collect data, or you will end up comparing a 600-word news brief to a 4,000-word investigation and drawing nonsense conclusions.
- A measurement method. Four realistic options, and the right one depends on who has to live with the reporting.
How to record the scrolling of a website: four methods compared
GA4 has no scroll depth report of its own. What it has is a scroll event that fires at thresholds when GA4’s own enhanced measurement setting is on, and the ability to accept custom events pushed to the data layer. Everything else is a way of producing those events.
| Method | Setup effort | What it gives you | Best for |
|---|---|---|---|
| GA4 enhanced measurement scroll events | Lowest — a toggle in the data stream | Threshold events at 25/50/75/100, no control over parameters | Getting a baseline this week with no engineering time |
| Google Tag Manager built-in scroll trigger | Low — no code, UI only | Threshold events with your own event name and parameters | Most newsrooms; the standard choice |
| Custom JavaScript snippet | Medium — a snippet and a deployment | Maximum scroll depth, active reading time, per-section depth | Long-form sites that need section-level data or dwell time |
| Visual tool (Clarity, Hotjar, Smartlook) | Low to install, high in page weight | Scroll maps and session recordings rather than clean numbers | Finding why people drop off, not what the average is |
If you already have a GA4 event called scroll_depth firing at four thresholds, skip ahead to Step 4.
Step-by-Step: How to Track Scroll Depth on Long Stories in GA4
Step 1: Define the long-story pages and your success metric
Pick 20 to 30 comparable stories. Comparable matters more than numerous, because scroll depth percentages are not comparable across pages of different lengths.
Then choose one metric before you look at any data. For most newsrooms the useful choices are:
- Reach of the reporting — did readers get to the part with the numbers in it? Milestone at 75% is the honest test here.
- Completion — what share hit 100%? Useful for explainers and short investigations, misleading on anything with a paywall or infinite feed at the foot.
- Retention — what share reached the halfway mark? The most stable number across story types.
Write the metric down somewhere the editorial team can see it. Half of the disappointment people report about scroll depth comes from three people each picking a different success definition in a meeting.
Step 2: Check your analytics setup
Before you build anything, confirm three things in GA4:
- The measurement ID is loading on a story page. Open the page source and search for
G-, or use GA4 DebugView with Tag Assistant running and reload the page. - The web data stream is the one receiving events. GA4 Admin, then Data streams, then Web. A story published under a different property than the homepage is a common and invisible failure.
- Enhanced measurement is set as you want it. Under the stream, Additional settings, the Page views section has a Scroll events option that reports the four standard thresholds. It is a reasonable starting point, and it is not the setup described in most of the guides that still rank for this query — several of them walk you through Universal Analytics tag types that were retired years ago.
Menu labels shift between GA4 interface versions. If you cannot find a setting described below, check the Admin breadcrumb for the property that actually holds your stream rather than the default one.
Step 3: Add scroll-depth tracking milestones
Use Google Tag Manager’s built-in scroll variables so you write no code at all.
In your container, open Variables → New and configure the three scroll variables:
- Scroll Depth Threshold — a number variable, 25.
- Scroll Depth Direction — a string variable set to
vertical. - Scroll Depth Percentage — the built-in variable that reports the actual percentage reached, which is different from your threshold.
Then create the trigger: Triggers → New → Scroll Depth, condition Scroll Depth Direction equals vertical and Scroll Depth Threshold greater than or equal to 25. Copy the trigger four times with thresholds of 25, 50, 75 and 100.
Attach a GA4 Event tag to each one. In the tag’s event parameters set:
- Event name:
scroll_depth percent_scrolled={{Scroll Depth Threshold}}scroll_unit=percent(a text variable containing the word percent, so GA4 does not try to treat it as a number)
The four separate tags matter. If you send one generic event, every hit looks identical in reports and you cannot build a funnel of thresholds.
For long stories I would go one step further and record depth per section. This is the number that actually answers “where do readers stop”, and no competitor page covering this query mentions it:
<script>
(function () {
var sections = document.querySelectorAll('[data-story-section]');
var seen = {};
if (!('IntersectionObserver' in window)) return;
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting || seen[entry.target.id]) return;
seen[entry.target.id] = true;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'story_section_view',
section_id: entry.target.id,
section_position: entry.target.getAttribute('data-story-section')
});
observer.unobserve(entry.target);
});
}, { threshold: 0.5 });
sections.forEach(function (section) { observer.observe(section); });
})();
</script>
Add a data-story-section attribute to each section wrapper in your story template. The 0.5 threshold means half of the section had to be on screen, which is a fairer test than “it appeared once” — a section flashing past at speed still counts as seen under the looser setting.
If you also want maximum depth and true active reading time, this dependency-free snippet pushes both. It uses a passive listener, recalculates page height on every check so lazy-loaded images do not corrupt the baseline, and only counts time while the tab is actually visible:
<script>
(function () {
var thresholds = [25, 50, 75, 100];
var fired = {};
var maxDepth = 0;
var activeMs = 0;
var visibleSince = document.visibilityState === 'visible' ? Date.now() : null;
var ticking = false;
function pageHeight() {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight,
document.documentElement.clientHeight
);
}
function depth() {
var total = pageHeight();
var travelable = total - window.innerHeight;
if (travelable <= 0) return 100;
var scrolled = window.scrollY + window.innerHeight;
return Math.min(100, Math.round((scrolled / total) * 100));
}
function check() {
ticking = false;
maxDepth = Math.max(maxDepth, depth());
thresholds.forEach(function (t) {
if (maxDepth >= t && !fired[t]) {
fired[t] = true;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'scroll_depth',
percent_scrolled: t,
max_scroll_percent: maxDepth,
time_on_page_active: Math.round(activeMs / 1000)
});
}
});
}
window.addEventListener('scroll', function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(check);
}, { passive: true });
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
visibleSince = Date.now();
} else if (visibleSince) {
activeMs += Date.now() - visibleSince;
visibleSince = null;
check();
}
});
})();
</script>
Two details worth copying exactly. The fired object stops the same threshold firing dozens of times during one scroll, and pageHeight() is read fresh on every check rather than cached at page load — that single line is the fix for most “my scroll depth looks wrong on long articles” complaints in analytics forums.
Step 4: Report the results in GA4
Default GA4 reporting will not get you there. A scroll event sitting in the Events report tells you almost nothing about which story or which threshold you are looking at, which is exactly the frustration readers on analytics forums describe.
Build the report in Reports → Explore:
- Free-form table, rows set to
Event name. - Filter to
Event nameexactly matchingscroll_depth. - Rows further split by
percent_scrolledso the four milestones become separate rows. - Columns set to
Event countand User count, not just event count. Counts inflate with refreshes and repeated scrolling; users is the honest denominator. - Add the page path or a story identifier as a second breakdown dimension so you get a per-story table rather than a site average.
Split every view by device. Mobile depth runs structurally lower than desktop on long stories, for the boring reason that phones are held in one hand and stories get read on the train. Blending the two produces an average that describes nobody.
For section-level data, filter story_section_view and break down by section_id. That single report usually ends more arguments about a story’s structure than any interview ever did.
Step 5: Validate the data and use it
Test before you report. This step is ten minutes and it is the one people skip.
- Open GA4 → Admin → DebugView. In GTM, preview your container and enable Tag Assistant’s GA4 debug mode so the events show up there too.
- Load a story and scroll slowly past 25%, 50%, 75% and the bottom.
- Confirm each milestone arrives once, with the correct
percent_scrolledvalue, and thatmax_scroll_percentclimbs smoothly rather than jumping. - Then verify in Admin → Data display → Debug events after the fact, or wait for the data to appear in the Events report before building anything on top of it.
If a milestone is missing, work down this list: the trigger’s threshold variable is set to the wrong number, the tag is not attached to that specific trigger, or consent mode is blocking analytics_storage for a share of your readers — which quietly deletes events from your numbers while leaving them visible in Tag Assistant.
How to read the numbers on a long story
The widely quoted figure is around 53% average scroll, which comes from general web content rather than journalism, so treat it as a reference point and not a target. Page length moves it: shorter pieces run higher, longer features run lower, and comparing the two is meaningless.
| Bucket | What it usually means on a long story | What to do |
|---|---|---|
| Under 25% | The headline promised something the page did not deliver, or the story opens with a wall of text above any proof | Check the headline against the first screen; add a summary box or a visual above the fold |
| 25% to 50% | Readers are leaving during the nut graf or the first data section | Look at what sits at that point. Usually a dense block, an autoplaying embed or a broken chart |
| 50% to 75% | The opening half works, the middle does not | Check for a paywall, newsletter prompt or an image that pushed content down as it loaded |
| 75% to 100% | Only a small committed core finishes long stories, which is normal | Do not rewrite. Make sure the conclusion and any call to action sit inside the 75% band |
That last row is worth repeating: a low 100% figure on a 4,000-word investigation is not a failure. Rewriting a story because most readers leave before the end is how you shorten the reporting that made people come in the first place.
Common Mistakes
Treating 100% as the goal. Completion is a threshold, not a quality score. A story with a strong 75% figure and a faithful conclusion is performing better than one where everyone reaches the bottom and remembers nothing.
Comparing percentages across different story lengths. This is the central problem on any long-form site, and no amount of clever reporting fixes it. Compare a 600-word brief to a 4,000-word investigation and the number you produce is arithmetic, not insight.
Setting the page height once at page load. Lazy-loaded images and embeds expand the page after the first paint, so a cached baseline makes the last quarter of the story appear further down than it is. Recalculate height on every check, as the snippet above does.
Forgetting infinite scroll and feed items below the story. On sites that load the next story after the current one, 100% scroll depth stops meaning “finished this story”. Measure to the bottom of the story container instead of the document.
Ignoring sticky headers. A persistent donation banner or newsletter bar reduces the visible scrollable area. If your template hides it on scroll down, the net effect is small, but on templates that pin it you are measuring a viewport permanently shorter than the real one.
Reading depth on a paywalled story. If the wall truncates the page, 100% is unreachable and the depth numbers understate what readers actually consumed. Either measure to the paywall itself and label it clearly, or measure only the free portion as a funnel step toward signup.
Sending one event for all thresholds. Everything collapses into a single row in reports and you lose the shape of the drop-off, which is the only part of the data worth having.
Trusting unverified data. Check DebugView before you build a dashboard on top of anything. Consent mode blocking a share of events is invisible everywhere except the numbers themselves.
Two reporting habits help more than any single fix. Report by story template — features, investigations, explainers, opinion — rather than as one site average, because those four formats have genuinely different healthy numbers. And pair depth with something downstream: newsletter signup events, scroll plus conversion, paywall impressions against scroll depth to the wall. Depth alone tells you where people stop, which is genuinely useful, but it is a symptom rather than a cause.
Frequently Asked Questions
What is a good scroll depth?
Around 53% average scroll is the figure most often quoted, but it comes from general web content rather than journalism. Treat it as a reference point, not a target. Short pieces run well above it and long features run below it. Compare a story only against itself over time and against other stories of similar length and template.
Can GA4 track scroll depth on its own?
Not fully out of the box. GA4’s enhanced measurement setting records scroll events at 25, 50, 75 and 100 percent, but you get no control over parameters or section-level detail. For anything custom, connect GA4 through Google Tag Manager with a scroll trigger and a GA4 event tag, or push your own events to the data layer from a small script.
Why is my average scroll depth so low on long articles?
Three causes do most of the damage. Lazy-loaded images and embeds expand the page after the first measurement, so cached page height makes the bottom look further away than it is. Infinite scroll adds feed content below the story, inflating total page height. And a paywall truncates the page, making completion unreachable. Recalculating page height on every scroll check fixes the first one.
How do I track which section of a story readers stop at?
Give each section wrapper a data-story-section attribute in your story template, then observe those elements with IntersectionObserver and push a story_section_view event with the section id and its position. Filter that event in a GA4 Exploration broken down by section_id. Threshold 0.5 is a fair test: half the section had to be on screen.
What is the difference between scroll depth and a scroll map?
Scroll depth is a number: the percentage of page height reached, or the share of readers hitting each milestone. A scroll map is a visual record of where movement happened across many sessions, usually drawn as a heatmap over page content. The number tells you what happened. The map helps you work out why, which is why both are useful.
Does scroll depth affect SEO?
No direct effect, and be sceptical of anyone claiming otherwise. Scroll depth is a page-level engagement signal, not a ranking factor you can act on. It is still worth measuring because it tells you which parts of a story reach the audience, which feeds editorial decisions, audience development and where to place calls to action.
Conclusion
Start with the toggle in your GA4 data stream and one story you care about. You will have threshold data inside the hour, which is enough to confirm the plumbing before anyone builds a dashboard on it. From there, add the per-section observer to your template — that is the step that turns a percentage into an editorial tool.
Then pair scroll depth with time on page, active tab time, newsletter signups and paywall interactions, and treat each as a separate signal rather than one blended engagement score. The metric tells you where readers stop; it never tells you why, and no single number judges a story’s quality.
Change one thing at a time on a small set of stories, and as of 2026 most of the long-form measurement problems in GA4 are still implementation problems rather than reporting problems.


