Does Googlebot Scroll and Click in SPAs?

Developers often assume Googlebot behaves like a patient user — scrolling to the bottom of a feed, clicking “load more,” opening tabs to read what is inside. It does not. Understanding exactly what Googlebot interacts with is essential for anyone building a single-page app, and it is a core part of understanding Googlebot’s rendering pipeline. The short answer: Googlebot renders, but it does not interact.

Problem Statement

Googlebot’s rendering step loads your page, executes JavaScript, waits for the network to settle, and snapshots the DOM. What it does not do is simulate a human. It does not click buttons, tap, hover, submit forms, or scroll the way a person drags a scrollbar. Any content that only appears as a consequence of one of those gestures is invisible to indexing — it is simply not in the DOM at the moment the snapshot is taken.

This trips up three patterns constantly: content behind a click-to-load button, items appended by infinite scroll as the user reaches the bottom, and panels that fetch their contents only when a tab or accordion is opened. In each case the page looks fully populated to a human who interacts with it, and completely empty to a crawler that does not. The fix is not to make Googlebot behave like a user; it is to make the content present without the interaction.

What Googlebot captures versus what needs interaction Content rendered at load enters the DOM snapshot and is indexed, while content that requires a click, tab open, or scroll-to-bottom gesture is never triggered and stays out of the snapshot. Googlebot renders, then snapshots — it never interacts Captured at load → indexed content present in the initial render markup hidden only by CSS data fetched on load / network idle links as real anchor elements Needs interaction → missed click-to-load buttons tabs / accordions that fetch on open infinite scroll past the first batch hover / form submit reveals Move content across this line to make it indexable
The dividing line for indexing is load time: content present at render is captured, content behind a gesture is not.

Step-by-step fix

  1. Inventory your interaction-gated content. Walk each template and mark everything that only appears after a click, tab, hover, form submit, or scroll-to-bottom. These are your at-risk items — content a crawler will never trigger.

  2. Render content at load, hide it with CSS if needed. Googlebot reads content that is in the DOM even when it is visually hidden with display:none or an off-screen class. Move tab and accordion contents into the initial render and toggle visibility client-side, rather than fetching them on open.

  3. Back infinite scroll with real paginated URLs. Keep the scroll experience for users, but ensure each batch of items also exists at a crawlable URL — /feed/?page=2 or /feed/page/2/ — that renders its items on load. Link those pages with anchor elements so Googlebot can follow them.

  4. Replace click-to-load with load-then-reveal. For “load more” patterns, fetch the first meaningful set of items during the initial render so the primary content is present without a click. A button can still load additional items for users beyond that set.

  5. Use anchor elements for navigation. Googlebot discovers URLs from href attributes on real anchor tags. A div with a click handler that routes client-side is not a discoverable link; give every navigable destination a genuine anchor with an href.

The pagination fix in step 3 is the difference between a feed that exposes only its first batch and one whose every batch sits at a crawlable URL.

Infinite scroll versus paginated URLs for crawlability A feed built only on infinite scroll exposes just the first batch to Googlebot, while the same feed backed by paginated URLs makes every batch crawlable at a stable link. Two ways to build a feed — only one is fully crawlable Infinite scroll only batch 1 at load scroll batch 2, 3 … gesture never fires → missed Paginated URLs behind it /feed?page=1 /feed?page=2 /feed?page=3 each rendered at load crawlable anchor links
Infinite scroll alone exposes only the first batch; the same feed backed by paginated URLs makes every batch crawlable.

Validation

Confirm the at-risk content is in the rendered DOM without any interaction, exactly as Googlebot would see it.

Rendering with the Googlebot user agent, then asserting content without interacting Load the page as Googlebot with no click or scroll, then assert that feed items, a tab panel, and a next-page anchor are all present in the rendered DOM. Render as Googlebot, then assert — no click, no scroll goto(url, waitUntil: networkidle) Googlebot UA — never click, scroll, or hover [data-feed-item] count > 0 without scrolling #reviews-panel text present without a click a[rel=next] count > 0 crawlable anchor
Because the harness never clicks or scrolls, every assertion that passes is content Googlebot would also capture at load.
// interaction-independent-render.mjs — verify content exists without clicking or scrolling.
import { chromium } from 'playwright';

const GOOGLEBOT_UA =
  'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) ' +
  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36 ' +
  '(compatible; Googlebot/2.1; +http://www.google.com/bot.html)';

const url = process.argv[2];
const browser = await chromium.launch();
const page = await browser.newContext({ userAgent: GOOGLEBOT_UA }).then((c) => c.newPage());

// Render and snapshot WITHOUT clicking, scrolling, or hovering — mirror Googlebot.
await page.goto(url, { waitUntil: 'networkidle' });

const feedItems = await page.locator('[data-feed-item]').count();
const tabPanelText = await page.locator('#reviews-panel').innerText().catch(() => '');
const nextPageLink = await page.locator('a[rel=next]').count();

console.assert(feedItems > 0, 'FAIL: no feed items present without scrolling');
console.assert(tabPanelText.trim().length > 0, 'FAIL: tab panel empty without a click');
console.assert(nextPageLink > 0, 'FAIL: no crawlable next-page anchor for the feed');
console.log(`feed items at load: ${feedItems}, next-page anchors: ${nextPageLink}`);

await browser.close();

Because the script never calls click() or scrolls, a passing run proves the content is reachable exactly as Googlebot renders it. Cross-check by inspecting the same URL in Search Console’s “View crawled page” — the rendered HTML there should contain the same items.

Common Pitfalls

  • Assuming Googlebot scrolls to load more. It renders with a tall viewport, which fires some lazy loaders, but it does not perform scroll gestures. Content that depends on reaching the bottom of a list is unreliable at best.
  • Fetching tab or accordion content on open. If the panel’s data is requested only when the user opens it, the crawler never opens it and never sees the data. Render it at load and hide it with CSS.
  • Routing through non-anchor elements. A clickable div is not a link Googlebot can follow. Navigation must use anchor elements with real href values.
  • Relying on hover or focus to reveal text. Googlebot does not hover or focus. Any content that only appears on those events is missed.
  • Confusing “visible” with “indexable.” CSS-hidden content in the DOM is indexable; interaction-gated content absent from the DOM is not. Presence in the rendered DOM is what counts.

That last pitfall is the whole decision in one question: not “is it visible?” but “is it in the rendered DOM at load?”

Decision tree for whether Googlebot will see an element If an element is present in the rendered DOM at load it is indexed even when CSS-hidden, but if it only appears after a gesture it is not indexed. Will Googlebot see this element? An element in your app Present in the rendered DOM at load time? yes no Indexed visible or CSS-hidden, both count Not indexed gated behind click, scroll, or hover
The single test for indexability is presence in the rendered DOM at load — not whether the element is visible.

Frequently Asked Questions

Does Googlebot scroll down a page to load lazy content?

Googlebot does not scroll like a user. It renders with a tall virtual viewport so that content lazy-loaded on scroll often triggers anyway, but content that depends on real scroll-position events or on reaching the bottom of an infinite list is unreliable. Anything that only loads after a genuine scroll gesture may never enter the rendered DOM.

Does Googlebot click buttons or tabs to reveal content?

No. Googlebot does not click, tap, hover, or perform any user interaction. Content hidden behind a click-to-load button, a tab, or an accordion that fetches on open will not be seen unless it is present in the rendered DOM at load time, even if visually hidden with CSS.

How do I make infinite-scroll content indexable?

Back the infinite scroll with real paginated URLs that render their items server-side or on load, and link them with anchor tags Googlebot can follow. The scroll experience stays for users, while each page of items is independently crawlable at a stable URL rather than depending on a scroll event that Googlebot never fires.

← Back to Understanding Googlebot’s Rendering Pipeline