Textualize/textual · error · OnDecoratorError

The message class must have a 'control' to match with the on

Error message

The message class must have a 'control' to match with the on decorator

What it means

The @on decorator lets you bind handlers to message attributes via CSS-like selectors, e.g. @on(Input.Submitted, "#search"). The special pseudo-selector "control" (e.g. @on(Button.Pressed, "save")) is only legal when the message class overrides the inherited Message.control property. If the message type still uses the base Message.control, Textual cannot resolve what 'control' the selector refers to, so it raises OnDecoratorError at decoration time.

Source

Thrown at src/textual/_on.py:69

    Args:
        message_type: The message type (i.e. the class).
        selector: An optional [selector](/guide/CSS#selectors). If supplied, the handler will only be called if `selector`
            matches the widget from the `control` attribute of the message.
        **kwargs: Additional selectors for other attributes of the message.
    """

    selectors: dict[str, str] = {}
    if selector is not None:
        selectors["control"] = selector
    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"):

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Add a control property override to your message class returning the widget the message is about
  2. Use a different supported attribute from the message's ALLOW_SELECTOR_MATCH (or none) in the decorator
  3. If you meant widget-id matching, ensure you are using a message type like Button.Pressed that implements control

Example fix

# before
class Saved(Message):
    pass

@on(Saved, "save")  # OnDecoratorError
def handle(self, event: Saved) -> None: ...

# after
class Saved(Message):
    def __init__(self, button: Button) -> None:
        super().__init__(button)

    @property
    def control(self) -> Button:
        return self._sender if isinstance(self._sender, Button) else None  # or stored ref
Defensive patterns

Strategy: type-guard

Validate before calling

from textual.message import Message

def supports_control(msg_type: type[Message]) -> bool:
    return msg_type.control is not Message.control

Type guard

def has_control_override(msg_type: type) -> TypeGuard[type]:
    from textual.message import Message
    return getattr(msg_type, "control", None) is not Message.control

Try / catch

raise OnDecoratorError only at import; guard by asserting supports_control(MyMsg) in unit tests before shipping decorators

Prevention

When it happens

Trigger: Writing @on(MyMessage, "something") where MyMessage subclasses Message (directly or via a base that never defines control) and therefore has no control property override; using @on(Button.Pressed, "save") style control selectors with a custom message type that forgot to implement control; decorating a method on a custom message class that was copied without the control property.

Common situations: Defining custom messages for custom widgets and assuming control matching works like it does for Button.Pressed/Input.Submitted; upgrading apps where a message hierarchy changed to no longer inherit a control override; matching on a Select/Tab-like message whose library class does define control but a user subclass shadows it away.

Related errors


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