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
- Add a control property override to your message class returning the widget the message is about
- Use a different supported attribute from the message's ALLOW_SELECTOR_MATCH (or none) in the decorator
- 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 creating custom messages with control matching, always implement the control property
- Unit-test module import so decorator errors fail fast in CI
- Copy the message-class pattern from Button.Pressed when adding control
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
- The attribute {attribute!r} can't be matched; have you added
- No handler for {event_name!r}
- Multiple handlers for key press {key_name!r}.\nWe found both
- Unable to parse selector {css_selector!r} for {attribute}; c
- 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/64c86270ad499fd2.
Report an issue: GitHub.