squirrelscan
GitHub

Hidden text and links: what Google's spam policy covers

Text a crawler reads and a visitor cannot see is a spam policy violation. squirrel finds it while exempting screen-reader and collapsed-UI patterns.

What hidden text is

Hidden text is content present in the markup and invisible on screen. Google’s spam policies treat text or links visible to crawlers but hidden from visitors as deceptive, and pages doing it can lose rankings or be removed from search.

Most hiding is legitimate. The screen-reader-only pattern deliberately hides text visually while leaving it available to assistive technology. A collapsed accordion, a closed dialog and an inactive tab panel are all hidden until the user opens them. What is not legitimate is a block of keywords or links behind display: none, a zero font size, an off-screen offset or same-on-same colour, put there for a crawler.

What squirrel checks

The rule runs on every crawled page and emits a single check named hidden-text:

  • Skipped when there is no <body>. Message: No body content to analyze.
  • Skipped when a work budget cut the scan short and the part scanned held nothing. Message: Page too large to scan in full; no hidden text in the part scanned.
  • Pass when the whole page was scanned and nothing qualified. Message: No hidden text or links detected.
  • Warn when hidden elements were found. Message: 3 hidden element(s) containing 240 characters of text.
  • Fail when the hidden links reach min_hidden_links. Message: 3 hidden element(s) containing 14 link(s) and 240 characters of text.

Each item names the element’s selector and the techniques that hid it, and carries the character count, link count and a markup snippet. Hidden links are the stronger spam signal, which is why they alone escalate the finding to a failure.

The walk stops after 20,000 elements, and stylesheets are read up to 400,000 characters or 4,000 rules. The finding’s details carry scanTruncated, which is true when either budget cut the scan short, because what lies past it was never inspected. When a cut-short scan found nothing, the rule skips the page rather than passing it: a pass would say the page is clean, and on a re-audit it would close an earlier finding that may sit past the budget. The skip has skipReason: "scan-truncated", the page counts as not evaluated, and its earlier findings stay open.

Hiding is read from inline style attributes and from <style> rules whose selector is a single class or id. Comments are dropped from a selector first, as a browser does, so /* Header */ .promo { display: none } is a single class rule, and .a /* x */ .b is still a descendant selector. Stylesheets are read the way a browser tokenizes them: a {, } or ; inside a string, a comment, an unquoted url(...), a ( or [ block, or after a backslash does not end a rule or a declaration. A sheet such as .a{background:url(}.b{display:none}.c{)} is one rule to a browser, so it hides nothing here either.

An element is exempt before any technique is considered when it carries a known screen-reader-only class such as sr-only, visually-hidden or screen-reader-text, or one you add through a11y_classes, and when it is marked as intentionally hidden UI through aria-hidden, the hidden attribute, or a role such as tabpanel or dialog. An element must hold at least min_hidden_chars characters to be reported. Severity for the rule is warning, weight 6.

How to fix it

<span class="sr-only">Opens in a new window</span>

Delete the hidden block, or make it genuinely visible. For text meant only for screen readers, use the standard visually-hidden pattern, a clipped one-pixel box or a class named sr-only or visually-hidden, so it is recognisable as an accessibility affordance rather than as hiding.

For UI that starts collapsed, mark the hidden state explicitly with aria-hidden, the hidden attribute, or a role such as tabpanel or dialog. That is what makes the state read as intentional.

Rule IDcontent/hidden-text
CategoryContent
ScopePer-page
Severitywarning (escalates to a failure when hidden links are involved)
Weight6/10

Options

OptionTypeDefaultDescription
min_hidden_charsinteger, 1 or more50Characters of hidden text on one element before it is reported
min_hidden_linksinteger, 1 or more10Hidden links before the finding escalates from a warning to a failure
offscreen_px_thresholdnumber-999Offsets at or below this many pixels count as pushed off-screen
a11y_classesarray of strings[]Extra screen-reader-only class names to treat as legitimate
safe_classesarray of strings[]Extra class names to exempt from hidden-text reporting

Configuration example

[rule_options."content/hidden-text"]
min_hidden_chars = 100
a11y_classes = ["u-hide-visually"]
safe_classes = ["mega-menu-panel"]

Enable / disable

Disable this rule

[rules]
disable = ["content/hidden-text"]

Disable all Content rules

[rules]
disable = ["content/*"]

Enable only this rule

[rules]
enable = ["content/hidden-text"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

Content findings ship in every audit next to the SEO, performance and agent experience rules. See Fix SEO issues with AI for how an agent works through a report.

Glossary: Cloaking, Keyword stuffing.

References

Check your site

Run squirrel audit https://example.com and open the Content section of the report. Every hidden element is listed with its selector, the technique used and a snippet. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close