By SitemapFixer Team
Updated April 2026

Hidden Text SEO: What Google Penalises and What It Allows

Audit your site for spam-policy risksRun a free scan

The phrase "hidden text SEO" covers two completely different things that get conflated all the time. The first is a spam tactic from the 1990s — stuffing keyword-dense paragraphs into a page in a colour that matches the background, hoping search engines read them while users do not. The second is the modern reality: every accessible website hides content all the time, in tabs, accordions, modals, and screen-reader-only labels, and Google is fine with it. The line between the two is intent and the surrounding pattern, not the CSS property used. This guide walks through exactly what Google's spam policies define as hidden text, the patterns that get sites penalised, the patterns that are explicitly safe, and how to audit your own templates before something ships that you regret.

What Google's Spam Policies Actually Say About Hidden Text

Google's official spam policy on hidden text reads: "Hiding text or links in your content to manipulate Google Search rankings can be considered deceptive and is a violation of Google's spam policies." The key clause is "to manipulate Google Search rankings." Hiding content for legitimate UX, accessibility, or progressive disclosure reasons is not what the policy targets. Hiding it specifically to feed keywords to a crawler that no human will ever read is.

The policy lists specific examples Google considers violations: white text on a white background, text behind an image, text positioned off-screen using CSS, font-size set to 0 or 1px, and links concealed in characters like a single period or hyphen. These are the patterns that originated in the late 1990s when search engines indexed text without rendering pages, so a paragraph stuffed at the bottom of a template in color: #ffffff on a white page would feed keywords to the index without affecting how the page looked. That era ended around 2010 when Google began rendering pages with a real browser, but the patterns persist in legacy code, so the policy persists too.

Detection in 2026 is mostly automated. Google's Web Rendering Service runs a Chromium-based renderer over every indexed page, computes the final styles for every text node, and compares the rendered output to the raw HTML. When there is a meaningful gap — text in the DOM that ends up invisible in the rendered viewport — algorithms flag the page for further review and, in egregious cases, manual action. The signal is not "does this page use display:none?" — it is "does this page use display:none on keyword-stuffed paragraphs that have no UX trigger to reveal them?"

The Patterns Google Definitely Flags

These are the CSS and HTML patterns I have seen trigger manual actions or algorithmic suppression. None of them are subtle once you know to look for them, and all of them are easy to grep for in a templates folder.

Colour matching the background. The original sin of hidden text SEO. Even if the body background is technically transparent and the text colour is technically white, the rendered output is white-on-white. Google catches this on render. Variations include text in a slightly off-white that is below the contrast threshold (e.g., #fefefe on #ffffff) — equally caught by the renderer's computed-style check.

<!-- BAD: classic colour-matching hidden text -->
<div style="color: #ffffff; background: #ffffff;">
  cheap flights cheap hotels cheap car rental
  best flights best hotels best car rental
  discount flights discount hotels discount car rental
</div>

<!-- BAD: near-match below contrast threshold -->
<p style="color: #fafafa;">
  buy viagra cialis online no prescription pharmacy
</p>

<!-- BAD: invisible via opacity -->
<section style="opacity: 0;">
  <h2>SEO keywords list</h2>
  <p>cheap insurance quotes life insurance auto insurance...</p>
</section>

font-size: 0 or 1px. Used to cram keywords into a page where they are technically rendered but visually undetectable. The renderer's computed-style pass catches this trivially.

Off-screen positioning of keyword-stuffed blocks. A common 2008-era trick: position a div at left: -9999px or text-indent: -9999px and fill it with keyword copy. There is a legitimate use of this technique (image-replacement for headings — discussed below), but a 400-word paragraph indented off-screen is not image replacement, it is hidden text.

<!-- BAD: keyword paragraph hidden off-screen -->
<div style="position: absolute; left: -9999px; top: -9999px;">
  <h2>Keywords</h2>
  <p>plumber london emergency plumber london 24 hour plumber
     london cheap plumber london best plumber london...</p>
</div>

<!-- BAD: text-indent abuse on body copy -->
<p style="text-indent: -9999px;">
  Long keyword-stuffed paragraph nobody will ever see...
</p>

<!-- BAD: font-size: 0 keyword dump -->
<div style="font-size: 0;">
  buy buy buy buy buy buy buy keywords keywords keywords
</div>

display:none combined with keyword stuffing. This is the pattern that confuses people most. display:none on its own is not a penalty signal — it is the most common way to hide content on the modern web. The penalty pattern is display:none on content that exists only to feed the crawler, with no UI affordance to reveal it. A 2,000-word block of city + service permutations sitting in display:none with no expand button anywhere on the page is the signal Google's reviewers flag.

The Patterns Google Says Are Fine

This is where most of the anxiety in webmaster forums is unnecessary. John Mueller, Martin Splitt, and Gary Illyes have repeatedly confirmed in office hours, podcasts, and Search Central blog posts that the following patterns are not hidden text in the spam-policy sense:

Accordions, tabs, and progressive disclosure. Content hidden behind a click that the user can clearly trigger. FAQ accordions, tabbed product specs, "read more" expansions on long articles — all fine. Google indexes the content normally and weights it the same as content visible by default. The original 2014 advice that "tabbed content carries less weight" is no longer accurate; that change was made explicit when mobile-first indexing rolled out, because mobile UIs hide content by default for space reasons.

Modals and popovers triggered by user action. Same logic as accordions. Content in a dialog that opens on click is treated normally. The exception is interstitial modals that block the main content on mobile — those have their own penalty (the "intrusive interstitials" signal), but it is not a hidden-text issue.

Accessibility-only text (sr-only / visually-hidden). The standard utility class used by every accessibility-aware codebase to expose labels and context to screen readers without showing them visually. This is not just allowed — it is encouraged. Google specifically calls out alt text, ARIA labels, and visually-hidden helper text as legitimate uses of techniques that "technically" hide content.

/* GOOD: standard sr-only / visually-hidden utility */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Used for things like: */
/* <button aria-label="Close"><span class="sr-only">Close menu</span>X</button> */
/* <a href="/cart"><span class="sr-only">View cart</span><svg ... /></a> */

Lazy-loaded content sections. Content that loads as the user scrolls down or as the JS framework hydrates additional sections. Google's renderer waits for network and script activity before computing final styles, so lazy-loaded sections are seen and indexed normally — provided they actually become visible without further user input beyond scrolling.

JavaScript-rendered tabs and panels. Common in React/Vue/Svelte component libraries. The hidden tab content lives in the DOM but with display:none until clicked. Google renders the page, sees the tab structure, and indexes all panels. This is the same treatment as a static HTML accordion.

visibility:hidden vs display:none vs opacity:0

These three CSS techniques all hide content but differ in how the renderer treats them, and the differences matter for both UX and SEO.

display:none removes the element from the rendering tree entirely. It takes up no space, is not in the accessibility tree, and is not visible. Screen readers do not announce it. Google still parses it from the DOM and indexes the content; it is by far the most common way to implement tabs, accordions, and modal content.

visibility:hidden hides the element visually but keeps its layout space. The element is not in the accessibility tree (most screen readers ignore it), but the surrounding layout reserves space for it. Used less often than display:none in modern UIs, mainly for animation transitions where you want to keep the layout stable while fading content in.

opacity:0 renders the element fully — it occupies space, captures clicks (unless you also set pointer-events: none), and remains in the accessibility tree. Screen readers announce it. From a hidden-text-SEO perspective, opacity:0 with a large keyword block is the riskiest of the three because the element is technically rendered, fully laid out, and just transparent — exactly the pattern Google's rendering pipeline is built to catch.

None of the three is automatically a violation. All three are violations if the content they hide is keyword stuffing with no legitimate UX trigger. The CSS property is not the signal; the surrounding pattern is.

The Correct Pattern for Expandable Content

If you have ever worried whether your FAQ section is hurting SEO, stop. The native <details>/<summary> element and ARIA-pattern accordions are both fully indexable. Here is the canonical correct pattern:

<!-- GOOD: native disclosure widget, fully accessible and indexable -->
<details>
  <summary>How long does indexing take after I fix hidden text?</summary>
  <p>Most pages get re-crawled within 1–2 weeks. Manual actions
     require a reconsideration request that resolves in 2–4 weeks.</p>
</details>

<!-- GOOD: ARIA-pattern accordion (React/Vue) -->
<div class="accordion-item">
  <button
    aria-expanded="false"
    aria-controls="panel-1"
    id="accordion-1">
    Does display:none hurt SEO?
  </button>
  <div
    id="panel-1"
    role="region"
    aria-labelledby="accordion-1"
    hidden>
    <p>No, not on its own. The penalty is for hidden keyword
       stuffing, not for legitimate UX hiding.</p>
  </div>
</div>

Two things to note. First, the hidden attribute behaves like display:none — Google still indexes the content. Second, the button has a clear text label and an accessible state (aria-expanded) that signals it is an interactive control. The combination of those two details — content that toggles via an obvious user-facing affordance — is what differentiates this from a hidden keyword dump.

How JavaScript-Rendered Hidden Content Actually Gets Indexed

Many tabs, modals, and accordions in modern apps are not in the initial HTML — they are rendered by React, Vue, or Svelte after hydration. People worry this means Google does not see the content. It usually does, but the mechanism is worth understanding.

Google's indexing pipeline has two phases. First-wave indexing reads the raw HTML response. Second-wave indexing — the rendering pass — runs the page through a Chromium-based renderer that executes JavaScript, waits for network activity to settle, and produces a final DOM snapshot. Hidden content rendered by client-side JavaScript shows up in the second-wave snapshot, including tab panels and modals that exist in the rendered DOM with display:none. As long as the JS framework actually mounts the hidden content into the DOM (rather than only mounting the active tab and unmounting inactive ones), Google sees and indexes it.

The failure mode is when a framework lazy-mounts content only on user interaction — e.g., a tab that does not render its panel until the user clicks. In that case the panel never enters the DOM during Googlebot's render, and the content is genuinely invisible to indexing. This is not a hidden-text spam issue; it is a rendering issue. The fix is either server-side rendering, static generation of the full DOM (including inactive tabs), or pre-mounting all tab panels with display:none toggling.

// BAD: only the active tab is in the DOM — Google never sees inactive tab content
function Tabs({ activeTab, panels }) {
  return (
    <div>
      {/* tab buttons... */}
      <div>{panels[activeTab]}</div>  {/* only one rendered */}
    </div>
  );
}

// GOOD: all panels in the DOM, hidden via display:none
function Tabs({ activeTab, panels }) {
  return (
    <div>
      {/* tab buttons... */}
      {panels.map((panel, i) => (
        <div
          key={i}
          role="tabpanel"
          hidden={i !== activeTab}
        >
          {panel}
        </div>
      ))}
    </div>
  );
}

// Verify Google sees the content:
// 1. Open URL Inspection in Search Console
// 2. Click "View tested page" > HTML
// 3. Search for content from a non-active tab — it should be there

How to Audit Your Site for Hidden Text Problems

A quick three-step audit will catch nearly all genuine hidden-text issues without false-flagging legitimate accordions.

Step 1: Grep your templates for known bad patterns. The riskiest patterns leave fingerprints in CSS and inline styles. Run these searches across your templates:

# Find off-screen positioning that is not sr-only
grep -rE "(left|top): -9999px" ./src ./templates ./theme

# Find font-size: 0 or 1px
grep -rE "font-size:\s*(0|1px)" ./src

# Find white-on-white or near-match colour combinations
grep -rE "color:\s*#?(fff|ffffff|fefefe|fafafa)" ./src

# Find opacity:0 used on large blocks
grep -rE "opacity:\s*0[^.\d]" ./src

# Find text-indent: -9999px on non-image-replacement elements
grep -rE "text-indent:\s*-9999px" ./src

Step 2: Use Chrome DevTools Computed Styles on suspicious blocks. Open any page, right-click an area you suspect contains hidden content, and inspect. The Computed pane shows the final resolved styles — including display, visibility, opacity, and color. If a content-heavy element has display:none with no associated button or trigger, that is the signal to investigate.

Step 3: Run Lighthouse and check the SEO category. Lighthouse audits include a "Document avoids hiding content with [aria-hidden] or display:none on content meant for indexing" check that flags some patterns automatically. Combine that with a manual pass through Search Console's URL Inspection "View tested page" HTML view to confirm Google's rendered DOM matches what users see.

For larger sites, the simplest scaling approach is to crawl your sitemap with a tool that captures rendered DOM snapshots and flags content blocks above a threshold word count that resolve to display:none or off-screen positioning with no associated interactive trigger. SitemapFixer's scan checks for several of these patterns by default.

Recovering From a Manual Action for Hidden Text

If you receive a manual action notice in Search Console under "Hidden text and/or keyword stuffing", the recovery path is mechanical and works reliably if you do it properly.

Step 1: Read the manual action carefully. The notice tells you whether the action is site-wide or partial. Partial actions name affected URL patterns. Site-wide actions affect everything on the property.

Step 2: Identify every instance of hidden text. Use the audit techniques above plus a fresh-eyes review of any pages flagged in the action. Do not just unhide the offending content — keyword-stuffed paragraphs that become visible are still a thin/spammy content problem and may trigger a different manual action. Remove the content entirely.

Step 3: Document changes. Take screenshots and HTML diffs showing what was removed. Note dates of removal. You will reference these in the reconsideration request.

Step 4: Submit a reconsideration request. In Search Console under Manual actions, click "Request review". Be specific: explain what the violation was, when it was added, why (e.g., "a previous SEO contractor added hidden keyword blocks to all city pages"), what you removed, and what process changes prevent recurrence (CI/CD checks, code review). Vague reconsideration requests are routinely declined.

Step 5: Wait and verify. Most hidden-text reconsideration requests resolve within 2–4 weeks. If it is rejected, the response usually names URLs where Google still detects the issue — fix those and resubmit. If you stop receiving rejections but the action persists, the requests are queued; do not resubmit repeatedly, that slows things.

Related Guides

Find hidden-text and spam-policy risks on your site
Free analysis in 60 seconds
Analyze My Site Free
Related guides