Textualize/textual · error · OnDecoratorError
Unable to parse selector {css_selector!r} for {attribute}; c
Error message
Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors What it means
The string selectors passed to @on are parsed by Textual's CSS selector parser; if a selector contains a syntax error the parser raises TokenError, which @on converts to OnDecoratorError with the offending selector quoted. This happens at import/decoration time, before the app even runs. Typical causes are stray characters, unbalanced quotes/brackets, or using selector syntax the parser does not accept in this position.
Source
Thrown at src/textual/_on.py:80
if kwargs:
selectors.update(kwargs)
parsed_selectors: dict[str, tuple[SelectorSet, ...]] = {}
for attribute, css_selector in selectors.items():
if attribute == "control":
if message_type.control == Message.control:
raise OnDecoratorError(
"The message class must have a 'control' to match with the on decorator"
)
elif attribute not in message_type.ALLOW_SELECTOR_MATCH:
raise OnDecoratorError(
f"The attribute {attribute!r} can't be matched; have you added it to "
+ f"{message_type.__name__}.ALLOW_SELECTOR_MATCH?"
)
try:
parsed_selectors[attribute] = parse_selectors(css_selector)
except TokenError:
raise OnDecoratorError(
f"Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors"
) from None
def decorator(method: DecoratedType) -> DecoratedType:
"""Store message and selector in function attribute, return callable unaltered."""
if not hasattr(method, "_textual_on"):
setattr(method, "_textual_on", [])
getattr(method, "_textual_on").append((message_type, parsed_selectors))
return method
return decorator
View on GitHub (pinned to 06dbeef4bb)
Solutions
- Inspect the quoted selector in the message and fix the syntax: ids as "#name", classes as ".class", combined "#id.cls"
- If building selectors dynamically, validate/sanitize interpolated values and escape or strip '#'/'.'-prefixed data
- Check the Textual CSS selector docs for the supported subset before using advanced pseudo-class syntax
- Lint import time early (run the app once or import the module in tests) so decorator errors surface in CI
Example fix
# before @on(Input.Submitted, "#search"") # stray quote -> OnDecoratorError # after @on(Input.Submitted, "#search")
Defensive patterns
Strategy: validation
Validate before calling
from textual.css.tokenize import parse_selectors # or textual.css parse helper
def selector_is_valid(selector: str) -> bool:
try:
parse_selectors(selector)
return True
except Exception:
return False
# assert selector_is_valid("#search") before building @on decorators dynamically Type guard
def is_plain_selector(s: str) -> TypeGuard[str]:
return bool(s) and all(ch.isalnum() or ch in "_-" for ch in s.lstrip("#.")) Try / catch
validate dynamically built selectors with parse_selectors in tests; static decorators fail at import, so a simple import-the-module test catches them
Prevention
- Keep selectors simple: "#id", ".class", "#id.class"
- Sanitize data interpolated into selectors via f-strings
- Add an import-smoke test per module using @on to catch parse errors in CI
When it happens
Trigger: @on(Input.Submitted, "#search""), @on(Button.Pressed, "save"), @on(MyMsg, field="not a selector!"), unmatched quotes or brackets, accidentally passing a non-selector value like a variable holding None, or concatenating strings incorrectly so spaces/operators are malformed.
Common situations: Typos in selector strings; f-string-built selectors that interpolate None or data containing special characters; copying selectors from browser CSS that use unsupported pseudo-classes; quotes lost during code generation or templating.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- {duration!r} is not a valid duration.
- No handler for {event_name!r}
- The message class must have a 'control' to match with the on
- The attribute {attribute!r} can't be matched; have you added
- Can not create a worker from a non-async function unless `th
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/2eb3e4273459a8a9.
Report an issue: GitHub.