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
- 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).
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
- 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.
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
- Error while parsing css selector "${selector}". Did you mean
- Unexpected end of selector while parsing selector `${selecto
- Malformed selector: ${part.name}=${part.body}
- Error while parsing selector `${selector}`: ${e.message}
- Error while parsing selector `${selector}` - cannot use ${op
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/f594f1ff6224a994.
Report an issue: GitHub.