symfony/css-selector · error · SyntaxErrorException
Unexpected pseudo-element
Error message
Unexpected pseudo-element "::%s" found %s.
What it means
SyntaxErrorException thrown by Parser::parserSelectorNode() when a pseudo-element (e.g. ::before, ::after) appears anywhere other than as the final component of a compound selector chain. CSS grammar only allows a pseudo-element at the very end of a selector, so the parser rejects any selector where more components (combinators, classes, attributes) follow a pseudo-element.
Solutions
- Move the pseudo-element to the end of the full selector: 'div span::before' instead of 'div::before span'.
- If you need to style the pseudo-element of an inner element, split into separate selectors per target element.
- Validate user-supplied selectors (regex or a pre-parse check for '::' followed by more selector tokens) before passing to Parser::parse().
- Catch Symfony\Component\CssSelector\Exception\SyntaxErrorException and surface a clear message to the caller.
Example fix
// before
$cssSelector->parse('div::before span');
// after
$cssSelector->parse('div span::before'); Defensive patterns
Strategy: validation
Validate before calling
if (preg_match('/::[a-zA-Z-]+\s*[-.#\[>+~:]/', $selector) || preg_match('/:(before|after|first-line|first-letter)\s*[-.#\[>+~]/i', $selector)) {
throw new \InvalidArgumentException('Pseudo-element must be at the end of the selector: ' . $selector);
} Try / catch
try {
$nodes = (new Parser())->parse($selector);
} catch (SyntaxErrorException $e) {
// log and skip this selector
} Prevention
- Always place pseudo-elements last in generated selectors
- Validate user-supplied selectors with a regex for mid-selector '::' before parsing
- Never append classes/attributes after a pseudo-element token
When it happens
Trigger: Calling Parser::parse() (or parseSelectorList via parse) with a selector where a pseudo-element is followed by another selector part, e.g. 'div::before span', 'a::first-line.class', 'ul ::after > li'. The check at Parser/Parser.php:135-137 fires when $pseudoElement is already set and the loop continues past end-of-selector tokens (',', EOF, ')').
Common situations: Typo or misreading of CSS spec: putting pseudo-elements mid-selector such as '.menu::before .item', concatenating user-supplied selector strings, porting selectors that browsers leniently accept, or generating selectors programmatically with the pseudo-element inserted before combinators.
Related errors
- Got immediate child pseudo-element
- Unclosed/invalid string at
- String not allowed as function argument.
- Got too deeply nested :has().
- Expected , but found.
AI-assisted analysis of symfony/css-selector@08e2905152 (2026-09-14).
Data as JSON: /api/errors/1b082ba0763452d3.
Report an issue: GitHub.
Appendix: source
Thrown at Parser/Parser.php:136
private function parserSelectorNode(TokenStream $stream, bool $isArgument = false, bool $insideRelativeSelector = false): Node\SelectorNode
{
[$result, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);
while (true) {
$stream->skipWhitespace();
$peek = $stream->getPeek();
if (
$peek->isFileEnd()
|| $peek->isDelimiter([','])
|| ($isArgument && $peek->isDelimiter([')']))
) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isDelimiter(['+', '>', '~'])) {
$combinator = $stream->getNext()->getValue();
$stream->skipWhitespace();
} else {
$combinator = ' ';
}
[$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
}
return new Node\SelectorNode($result, $pseudoElement);
}
/**
* @return list<array{0: string, 1: Node\SelectorNode}>View on GitHub (pinned to 08e2905152)