microsoft/playwright · error · InvalidSelectorError

Unsupported token "${unsupportedToken.toSource()}" while par

Error message

Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?

What it means

Thrown by parseCSS when the CSS tokenizer accepted the input but produced a token type that has no meaning inside a selector. Playwright pre-scans the token stream for tokens that are syntactically valid CSS but never valid in a selector grammar: at-keywords (@), semicolons, curly braces, url()/bad-url, percentage, CDATA comments (<!--/-->) and bad-string tokens. The hint 'Did you mean to CSS.escape it?' signals that a literal value leaked into the selector unescaped.

Source

Thrown at packages/isomorphic/cssParser.ts:71

  const unsupportedToken = tokens.find(token => {
    return (token instanceof css.AtKeywordToken) ||
      (token instanceof css.BadStringToken) ||
      (token instanceof css.BadURLToken) ||
      (token instanceof css.ColumnToken) ||
      (token instanceof css.CDOToken) ||
      (token instanceof css.CDCToken) ||
      (token instanceof css.SemicolonToken) ||
      // TODO: Consider using these for something, e.g. to escape complex strings.
      // For example :xpath{ (//div/bar[@attr="foo"])[2]/baz }
      // Or this way :xpath( {complex-xpath-goes-here("hello")} )
      (token instanceof css.OpenCurlyToken) ||
      (token instanceof css.CloseCurlyToken) ||
      // TODO: Consider treating these as strings?
      (token instanceof css.URLToken) ||
      (token instanceof css.PercentageToken);
  });
  if (unsupportedToken)
    throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`);

  let pos = 0;
  const names = new Set<string>();

  function unexpected() {
    return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`);
  }

  function skipWhitespace() {
    while (tokens[pos] instanceof css.WhitespaceToken)
      pos++;
  }

  function isIdent(p = pos) {
    return tokens[p] instanceof css.IdentToken;
  }

  function isString(p = pos) {

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Escape any dynamic value with CSS.escape() before interpolating: page.locator('div[class=' + CSS.escape(userInput) + ']').
  2. Switch to a locator that takes the value as data, not as CSS syntax: page.getByText(userInput), page.getByRole(..., {name: userInput}), or an attribute locator with a plain string.
  3. If you genuinely need special characters as literals, wrap them in an attribute selector with a quoted string: [attr="value"] rather than a bare token.
  4. Re-read the offending selector printed in the message and remove the offending token type (the token's toSource() is shown).

Example fix

// before
const cls = getDynamicClass(); // e.g. '50%off'
await page.locator('div.' + cls).click(); // Unsupported token '%'

// after
await page.locator('div[class=' + CSS.escape(cls) + ']').click();
// or prefer data over syntax
await page.getByText(cls).click();
Defensive patterns

Strategy: try-catch

Validate before calling

function isValidCssSelector(sel: string): boolean {
  // Reject tokens Playwright's CSS parser refuses outright.
  if (/[;{}%@]|^(<!--|-->)|url\(/i.test(sel)) return false;
  try { document.querySelector(sel); return true; } catch { return false; }
}

Try / catch

import { isInvalidSelectorError } from '@playwright/test';
try {
  await page.locator(maybeSelector).click();
} catch (e) {
  if (isInvalidSelectorError(e)) { /* bad selector path */ }
  else throw e;
}

Prevention

When it happens

Trigger: Calling page.locator()/page.$()/$$ with a selector that contains ';', '{', '}', '%', '@keyword', 'url(...)', unterminated quotes, or '<!--'/'-->'. Most often: interpolating dynamic/external text or attribute values into a CSS selector without escaping (e.g. page.locator('div[class=' + userInput + ']') where userInput contains ';' or '%').

Common situations: Passing user-supplied or scraped text straight into a selector string; copy-pasting a CSS rule (with its braces) instead of just the selector; selectors built from data containing special chars like '%' in width values or '@' in emails; version migrations where a previously-tolerated string now hits the strict token allowlist.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/f594f1ff6224a994. Report an issue: GitHub.