{"record":{"id":"f594f1ff6224a994","repo":"microsoft/playwright","slug":"unsupported-token-unsupportedtoken-tosource","errorCode":null,"errorMessage":"Unsupported token \"${unsupportedToken.toSource()}\" while parsing css selector \"${selector}\". Did you mean to CSS.escape it?","messagePattern":"Unsupported token \"(.+?)\" while parsing css selector \"(.+?)\"\\. Did you mean to CSS\\.escape it\\?","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/cssParser.ts","lineNumber":71,"sourceCode":"  const unsupportedToken = tokens.find(token => {\n    return (token instanceof css.AtKeywordToken) ||\n      (token instanceof css.BadStringToken) ||\n      (token instanceof css.BadURLToken) ||\n      (token instanceof css.ColumnToken) ||\n      (token instanceof css.CDOToken) ||\n      (token instanceof css.CDCToken) ||\n      (token instanceof css.SemicolonToken) ||\n      // TODO: Consider using these for something, e.g. to escape complex strings.\n      // For example :xpath{ (//div/bar[@attr=\"foo\"])[2]/baz }\n      // Or this way :xpath( {complex-xpath-goes-here(\"hello\")} )\n      (token instanceof css.OpenCurlyToken) ||\n      (token instanceof css.CloseCurlyToken) ||\n      // TODO: Consider treating these as strings?\n      (token instanceof css.URLToken) ||\n      (token instanceof css.PercentageToken);\n  });\n  if (unsupportedToken)\n    throw new InvalidSelectorError(`Unsupported token \"${unsupportedToken.toSource()}\" while parsing css selector \"${selector}\". Did you mean to CSS.escape it?`);\n\n  let pos = 0;\n  const names = new Set<string>();\n\n  function unexpected() {\n    return new InvalidSelectorError(`Unexpected token \"${tokens[pos].toSource()}\" while parsing css selector \"${selector}\". Did you mean to CSS.escape it?`);\n  }\n\n  function skipWhitespace() {\n    while (tokens[pos] instanceof css.WhitespaceToken)\n      pos++;\n  }\n\n  function isIdent(p = pos) {\n    return tokens[p] instanceof css.IdentToken;\n  }\n\n  function isString(p = pos) {","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/isomorphic/cssParser.ts#L53-L89","documentation":"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.","triggerScenarios":"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 '%').","commonSituations":"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.","solutions":["Escape any dynamic value with CSS.escape() before interpolating: page.locator('div[class=' + CSS.escape(userInput) + ']').","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.","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.","Re-read the offending selector printed in the message and remove the offending token type (the token's toSource() is shown)."],"exampleFix":"// before\nconst cls = getDynamicClass(); // e.g. '50%off'\nawait page.locator('div.' + cls).click(); // Unsupported token '%'\n\n// after\nawait page.locator('div[class=' + CSS.escape(cls) + ']').click();\n// or prefer data over syntax\nawait page.getByText(cls).click();","handlingStrategy":"try-catch","validationCode":"function isValidCssSelector(sel: string): boolean {\n  // Reject tokens Playwright's CSS parser refuses outright.\n  if (/[;{}%@]|^(<!--|-->)|url\\(/i.test(sel)) return false;\n  try { document.querySelector(sel); return true; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"import { isInvalidSelectorError } from '@playwright/test';\ntry {\n  await page.locator(maybeSelector).click();\n} catch (e) {\n  if (isInvalidSelectorError(e)) { /* bad selector path */ }\n  else throw e;\n}","preventionTips":["Never interpolate raw user/data values into a CSS string — use CSS.escape or a data-based locator.","Prefer getByText/getByRole/getByLabel over hand-built CSS for dynamic values.","Validate selectors in dev/test fixtures before they reach production."],"tags":["css","selector","parsing","invalid-selector","user-input"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}