Skip to content

querySelector throws SyntaxError on selectors containing CSS hex escapes (e.g. #\39 7e...) #2088

@lukebolandt1

Description

@lukebolandt1

Describe the bug

querySelector and querySelectorAll throw a SyntaxError (or silently return null) when the selector contains CSS hex escape sequences produced by CSS.escape().

When an element ID starts with a digit (e.g. a UUID like 97e356d3-...), CSS.escape() correctly produces a hex escape: \39 7e356d3-.... Per CSS Syntax 3 §4.3.7, the \39 is 1-6 hex digits representing code point U+0039 ('9'), and the trailing space is a delimiter consumed by the tokenizer — it is not a descendant combinator.

happy-dom's SelectorParser uses SELECTOR_GROUP_REGEXP to split selectors at combinators, but this regex is not escape-aware. It sees the space inside \39 and incorrectly treats it as a descendant combinator, splitting #\39 7e356d3-... into two fragments: #\39 and 7e356d3-....

Additionally, ESCAPED_CHARACTER_REGEXP (/\\/g) only strips backslashes rather than decoding CSS hex escapes (\39 9), so even selectors that don't throw will never match.

The SyntaxError throw was introduced in v20.6.3 (commit e6a64da, PR #2068). The underlying hex escape parsing bug existed before that, but manifested as a silent null return rather than a hard error.

To Reproduce

const id = '97e356d3-601d-42ca-9c13-3446228274ac';

const container = document.createElement('div');
const child = document.createElement('span');
child.id = id;
container.appendChild(child);

// CSS.escape() produces: \39 7e356d3-...
container.querySelector(`#${CSS.escape(id)}`);
// Throws: SyntaxError: Failed to execute 'querySelectorAll' on 'Element':
//   '#\39 7e356d3-...' is not a valid selector.

A reproduction test is available at: https://stackblitz.com/edit/vitejs-vite-5gnp3mau?file=src%2Fcss-escape.test.ts

Expected behavior

  1. querySelector(#${CSS.escape(id)}) should not throw — the selector is valid CSS.
  2. It should find and return the element — a spec-compliant parser decodes \39 as '9', producing the ID 97e356d3-....

Both behaviors work correctly in Chrome, Firefox, and Safari.

Device:

  • OS: Any (Node.js / Bun)
  • Browser: happy-dom
  • Version: >=20.6.3 throws SyntaxError; <20.6.3 silently returns null (doesn't throw but doesn't find the element either)

Additional context

Spec references:

There are two things to fix:

  1. SELECTOR_GROUP_REGEXP (or its replacement) must be escape-aware: when encountering \ followed by 1-6 hex digits and an optional space, that space must not be treated as a combinator.
  2. ESCAPED_CHARACTER_REGEXP must be replaced with a proper CSS hex escape decoder that converts \XX to String.fromCodePoint(parseInt('XX', 16)) per the spec, rather than just stripping backslashes.

Workaround: Use attribute selectors instead of CSS.escape():

container.querySelector(`[id="${id}"]`);

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions