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

  1. Inspect the quoted selector in the message and fix the syntax: ids as "#name", classes as ".class", combined "#id.cls"
  2. If building selectors dynamically, validate/sanitize interpolated values and escape or strip '#'/'.'-prefixed data
  3. Check the Textual CSS selector docs for the supported subset before using advanced pseudo-class syntax
  4. 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

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

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/2eb3e4273459a8a9. Report an issue: GitHub.