<button invoketarget="my-dialog">This opens a dialog</button>
<dialog>This is the dialog</dialog>
Adding invoketarget
and invokeaction
attributes to <button>
and
<input type="button">
/ <input type="reset">
elements would allow authors to
assign behaviour to buttons in a more accessible and declarative way, while
reducing bugs and simplifying the amount of JavaScript pages are required to
ship for interactivity. Buttons with invoketarget
will - when clicked,
touched, or enacted via keypress - dispatch an InvokeEvent
on the element
referenced by invoketarget
, with some default behaviours.
In addition, this proposals seeks to add an interesttarget
attribute to
interactive elements: starting with <button>
,
<input type="button">
/<input type="reset">
, and <a>
(perhaps expanding to
<area>
, <input>
, <textarea>
, <select>
, <summary>
or maybe more, see
#14). This
would allow disclosure of high fidelity tooltips in a more accessible and
declarative way. Elements with interesttarget
will - when hovered, long
pressed, or focussed - dispatch an InterestEvent
on the element referenced by
interesttarget
, with some default behaviours.
All elements within the DOM are capable of having interactions added to them. A
long while ago this took the form of adding inline JavaScript to an event
attribute, such as <button onclick="other.open()"></button>
. Inline JavaScript
has (rightly so) fallen out of favour due to the security and maintainability
concerns. Newer pages may instead introduce more JavaScript to imperatively
discover elements and call addEventListener('click', ...)
to invoke the same
behaviour. These patterns reduce developer experience and introduce more
boilerplate and friction, while remediating security and maintainability
concerns. Some frameworks attempt to reintroduce the developer experience of
inline handlers by introducing new JavaScript or HTML shorthands, such as
React's onclick={...}
, Vue's @click=".."
or HTMX's hx-trigger="click"
.
There has also been a rise in the desire to customise controls for components. Many sites, for example, introduce custom controls for File Uploads or dropdown menus. These often require a large amount of work to reintroduce the built in functionality of those controls, and often unintentionally sacrifice accessibility in doing so.
With the new popover
attribute, we saw a straightforward declarative way to
tell the DOM that a button was interested in being a participant of the popover
interaction. popovertarget
would indicate to a browser that if that button was
interacted with, it should open the element the popovertarget
value pointed
to. This allows for popovers to be created and interacted with - in an
accessible and reliable way - without writing any additional JavaScript, which
is a very welcome addition. While popovertarget
sufficiently captured the use
case for popovers, it fell short of providing the same developer & user
experience for other interactive elements such as <dialog>
, <details>
,
<video>
, <input type="file">
, and so on. This proposal attempts to redress
the balance.
- Invoke/Invoked/Invoking: The action of Invoking refers to a complete press
action of a button, using a Human Input Device (HID). If the HID is a mouse,
this would be a
click
event. If the HID is a touch screen, this would be a press using a stylus or finger. If the HID is a keyboard this would be theSpace
orEnter
(Carriage Return) key on the keyboard. For other HIDs such as eye tracking or pedals or game controllers, the equivalent expected "click" action should be used to invoke the element. - Interest/Shows Interest: The action of Interest refers to the user "landing"
on an element without invoking it, using a HID. If the HID is a mouse, this
would be a
hover
event. If the HID is a touch screen, this would be a long press on the element using a stylus or finger, if the HID is a keyboard, this would be when the element is focussed. For other HIDs such as eye tracking or pedals or game controllers, the equivalent "focus or hover" action is used to take interest on the element. - Loses/Lost Interest: The action of Loses Interest refers to the user "moving
away" from an element, or its interstee, using a HID; in other words
interest must be on a different element that is neither. Elements can only
Lose Interest if they are in the state of Showing Interest. If the HID is a
mouse, this would be a
mouseout
event. If the HID is a touch screen, this would be long pressing outside of the elements bounds. If the HID is a keyboard, this would be moving focus away from the element. For other HIDs such as eye tracking or game controllers, the equivalent "focusout or mouseout" action is used to Lose Interest on the element. - Invoker: An invoker is a button element (that is a
<button>
,<input type="button">
, or<input type="reset">
) that has aninvoketarget
attribute set. - Invokee: An element which is referenced to by an Invoker, via the
invoketarget
attribute. - Interestee: An element which is referenced to by an Interest element, via the
interesttarget
attribute.
In the style of popovertarget
, this document proposes we add invoketarget
,
and invokeaction
as available attributes to <button>
,
<input type="button">
and <input type="reset">
elements, as well as an
interesttarget
attribute to <button>
, <a>
, <area>
and <input>
elements.
interface mixin InvokerElement {
[CEReactions] attribute Element? invokeTargetElement;
[CEReactions] attribute DOMString invokeAction;
};
interface mixing InterestElement {
[CEReactions] attribute Element? interestTargetElement;
}
HTMLButtonElement extends InvokerElement
HTMLInputElement extends InvokerElement
HTMLButtonElement extends InterestElement
HTMLInputElement extends InterestElement
HTMLAnchorElement extends InterestElement
The invoketarget
value should be an IDREF pointing to an element within the
document. .invokeTargetElement
also exists on the element to imperatively
assign a node to be the invoker target, allowing for cross-root invokers (in
some cases, see
the popovertarget attr-asociated element steps
for more).
The invokeaction
(and the .invokeAction
reflected property) is a freeform
hint to the Invokee. If invokeaction
is a falsey value (''
, null
, etc.)
then it will default to 'auto'
. Values which are not recognised should be
passed verbatim, and should not be assumed to be auto
. This allows for custom
actions. Built-in interactive elements have built-in behaviours (detailed below)
which are determined by the invokeaction
but also Invokees will dispatch
events when Invoked, allowing custom code to take control of invocations without
having to manually wire up DOM nodes for the variety of invocation patterns.
The interesttarget
value should be an IDREF pointing to an element within the
document. .interestTargetElement
also exists on the element to imperatively
assign a node to be the invoker target, allowing for cross-root invokers (in
some cases, see
the popovertarget attr-asociated element steps
for more).
Elements with invoketarget
set will dispatch an InvokeEvent
on the Invokee
(the element referenced by invoketarget
) when the element is Invoked. The
InvokeEvent
's type
is always invoke
. The event also contains a
relatedTarget
property that will reference the Invoker element.
InvokeEvents
are always non-bubbling, cancellable events.
[Exposed=Window]
interface InvokeEvent : Event {
constructor(InvokeEventInit invokeEventInit);
readonly attribute Element relatedTarget;
readonly attribute DOMString type = "invoke";
readonly attribute DOMString action;
};
dictionary InvokeEventInit : EventInit {
DOMString action = "auto";
Element relatedTarget;
};
Elements with interesttarget
set will dispatch an InterestEvent
on the
Interestee (the element referenced by interesttarget
) when the element
Shows Interest or Loses Interest. When the element Shows Interest the
event type will be 'interest'
. If the element has Shown Interest, and
interest moves away from both the Interest Element and the Interestee, then
the element Loses Interest and an InterestEvent
with the type of
'loseinterest'
will be dispatched on the Interestee. The event also contains
a relatedTarget
property that will reference the Interest element.
InterestEvents
are always non-bubbling, cancellable events.
[Exposed=Window]
interface InterestEvent : Event {
constructor(DOMString type, InterestEventInit interestEventInit);
readonly attribute Element relatedTarget;
};
dictionary InterestEventInit : EventInit {
Element relatedTarget;
};
Both interesttarget
and invoketarget
can exist on the same element at the
same time, and both should be respected.
If an element also has both a popovertarget
and invoketarget
attribute, then
popovertarget
must be ignored: invoketarget
takes precedence. An element
with both interesttarget
and popovertarget
is valid and both actions will
work.
If a <button>
is a form participant, or has type=submit
, then invoketarget
must be ignored. interesttarget
is still valid in these scenarios.
If an <input>
is a form participant, or has a type
other than reset
or
button
, then invoketarget
must be ignored. interesttarget
is still valid
in these scenarios.
When pointing to a popover
, invoketarget
acts like much like
popovertarget
, allowing the toggling of popovers.
<button invoketarget="my-popover">Open Popover</button>
<!-- Effectively the same as popovertarget="my-popover" -->
<div id="my-popover" popover="auto">Hello world</div>
An interesttarget
allows for tooltips using popover
:
<button interesttarget="my-popover">Open Popover</button>
<div id="my-popover" popover="hint">Hello world</div>
When pointing to a <dialog>
, invoketarget
can toggle a <dialog>
's
openness.
<button invoketarget="my-dialog">Open Dialog</button>
<dialog id="my-dialog">
Hello world!
<button invoketarget="my-dialog" invokeaction="close">Close</button>
</dialog>
When pointing to a <details>
, invoketarget
can toggle a <details>
'
openness.
<button invoketarget="my-details">Open Details</button>
<!-- Can be used to replicate the `<summary>` interaction -->
<details id="my-details">
<summary>Summary...</summary>
Hello world!
</details>
Pointing an invoketarget
to an <input type="file">
acts the same as the
rendered button within the input; and can be used to declare a customised
alternative button to the input's button.
<button invoketarget="my-file">Pick a file...</button>
<input id="my-file" type="file" />
The <video>
and <audio>
tags have many interactions, here invokeaction
shines, allowing multiple buttons to handle different interactions with the
video player.
<button invoketarget="my-video">Play/Pause</button>
<button invoketarget="my-video" invokeaction="mute">Mute</button>
<video id="my-video"></video>
Invokers will dispatch events on the Invokee element, allowing for custom JavaScript to be triggered without having to wire up manual event handlers to the Invokers.
<button invoketarget="my-custom">Invoke a div... to do something?</button>
<button invoketarget="my-custom" invokeaction="frobulate">Frobulate</button>
<div id="my-custom"></div>
<script>
document.getElementById("my-custom").addEventListener("invoke", (e) => {
if (e.action === "auto") {
console.log("invoked an auto action!");
} else if (e.action === "frobulate") {
alert("Successfully frobulated the div");
}
});
</script>
Elements with Interest will dispatch events on the Interestee element, allowing for custom JavaScript to be triggered without having to wire up manual event handlers to the Interest elements.
<button interesttarget="my-custom">
When this button shows interest, the below div will display.
</button>
<div id="my-custom" hidden>Supplementary information</div>
<script>
const custom = document.getElementById("my-custom");
custom.addEventListener("interest", (e) => {
custom.hidden = false;
});
custom.addEventListener("loseinterest", (e) => {
custom.hidden = true;
});
</script>
Warning
This section is incomplete PRs welcome.
The Invoker implicitly receives aria-controls=IDREF
or aria-details=IDREF
(tbd) to expose the Invoker controls another element (the Invokee) for
instances where the invokee is not a sibling to the invoker element.
If the Invokee has the popover
attribute, the Invoker implicitly receives
an aria-expanded
state, as well as an aria-details
association (for
instances where the elements are not DOM siblings) which will match the state of
the popover's openness. It will be aria-expanded=true
when the popover
is
:popover-open
and aria-expanded=false
otherwise.
If the Invokee is a <details>
element the Invoker implicitly receives an
aria-expanded
state which will match the state of the Invokee's openness. It
will be aria-expanded=true
when the Invokee is open and
aria-expanded=false
otherwise.
If the Invokee is a <dialog>
element the Invoker implicitly receives an
aria-expanded
state which will match the state of the Invokee's openness. It
will be aria-expanded=true
when the Invokee is open and
aria-expanded=false
otherwise.
TBD: Accessibility attributes for interesttarget
.
Depending on the target set by invoketarget
, invoking the button will trigger
additional behaviours alongside the event dispatch. Invoking an invoketarget
will always dispatch a trusted InvokeEvent
, but in addition the following
table represents how invocations on specific element types are handled. Note
that this list is ordered and higher rules take precedence:
Invokee Element | action hint |
Behaviour |
---|---|---|
<* popover> |
'auto' |
Call .togglePopover() on the invokee |
<* popover> |
'hidePopover' |
Call .hidePopover() on the invokee |
<* popover> |
'showPopover' |
Call .showPopover() on the invokee |
<dialog> |
'auto' |
If the <dialog> is not open , call showModal() , otherwise cancel the dialog |
<dialog> |
'showModal' |
If the <dialog> is not open , call showModal() |
<dialog> |
'close' |
If the <dialog> is open , cancel the dialog |
<details> |
'auto' |
If the <details> is open , then close it, otherwise open it |
<details> |
'open' |
If the <details> is not open , then open it |
<details> |
'close' |
If the <details> is open , then close it |
<select> |
'auto' |
Open the <select> option picker UI |
<input> |
'auto' |
Call .showPicker() on the invokee |
<video> |
'auto' |
Toggle the .playing value |
<video> |
'pause' |
If .playing is true , set it to false |
<video> |
'play' |
If .playing is false , set it to true |
<video> |
'mute' |
Toggle the .muted value |
<audio> |
'auto' |
Toggle the .playing value |
<audio> |
'pause' |
If .playing is true , set it to false |
<audio> |
'play' |
If .playing is false , set it to true |
<audio> |
'mute' |
Toggle the .muted value |
<canvas> |
'clear' |
Remove all image data on the canvas (effectively (.clearRect(0, 0, width, height) ) |
<*> |
'toggleFullscreen' |
If the element is fullscreen, then exit, otherwise request to enter |
<*> |
'requestFullscreen' |
Request the element to enter into 'fullscreen' mode |
<*> |
'exitFullscreen' |
Request the element to exit 'fullscreen' mode |
Note
The above table is an attempt at wide coverage, but ideas are welcome. Please submit a PR if you have one!
Depending on the target set by interesttarget
, showing interest or losing
interest can trigger additional behaviours alongside the event dispatch.
Showing/losing interest on an interesttarget will always dispatch a trusted
InterestEvent
, but in addition the following table represents how interest on
specific element types are handled. Note that this list is ordered and higher
rules take precedence:
Interestee Element | Event Type | Behaviour |
---|---|---|
<* popover=hint> |
'interest' |
Call .showPopover() on the invokee |
<* popover=hint> |
'loseinterest' |
Call .hidePopover() on the invokee |
Note
The above table is an attempt at wide coverage, but ideas are welcome. Please submit a PR if you have one!
As the underlying system for invoke/interest elements uses event dispatch,
Custom Elements can make use of InvokeEvent
/InterestEvent
s for their own
behaviours. Consider the following:
<button
interesttarget="my-element"
invoketarget="my-element"
invokeaction="spin"
>
Spin the widget
</button>
<spin-widget id="my-element"></spin-widget>
<script>
customElements.define(
"spin-widget",
class extends HTMLElement {
connectedCallback() {
this.addEventListener("invoke", (e) => {
if (e.action === "spin") {
this.spin();
}
});
this.addEventListener("interest", (e) => {
this.style.transform = "rotate(1deg)";
});
this.addEventListener("loseinterest", (e) => {
this.style.transform = "rotate(0)";
});
}
},
);
</script>
While click
is a fairly well established name in the world of the web, it is
quite specific to certain types of HID and is not a term which encompasses all
viable methods of interaction. In addition a clickaction
attribute is deemed
to be a little too ambiguous as it conflates existing concepts. Given the
opportunity to supply a new name, invoke
was settled on.
Much like click
, hover
or focus
are specific to certain types of HID, and
are not terms which encompass all viable methods of interaction. Lots of
alternatives were discussed and
it was deemed that interest
is the best name to explain the concept of a
"hover or focus or equivalent".
Defaults for <form>
are intentionally omitted as this proposal does not aim to
replace Reset or Submit buttons. If you want to control forms, use those.
Defaults for <a>
are intentionally omitted as this proposal does not aim to
replace anchors. If you intend to produce a page navigation, use an <a>
tag.
This is by design, to allow for a "pit of success"; invoking actions on
non-button elements such as <div>
s or <a>
s creates many problems, especially
for non-interactive elements. While <a>
s are interactive, they should only
be used for page navigation and not for invoking other behaviours, and so
invoketarget
should not be allowed.
This is not added by design. Submit inputs already have a default action:
submitting forms. If you want a button to control the submission of a form, use
input[type=submit]
, if you want a button to control invocation of something
other than a form then you should use input[type=button]
.
It may stand to reason that if input[type=submit]
is excluded then so should
input[type=reset]
, however, there are valid use cases to resetting a form at
the same time as some other action, for example closing the dialog that contains
a form:
<dialog id="my-dialog">
<form>
<input type="text" />
<!-- This button closes the dialog _and_ resets the form -->
<input type="reset" invoketarget="my-dialog" value="Cancel" />
</form>
</dialog>
While invocation should only be limited to buttons, disclosure of supplementary information can be expanded to all interactive elements. There are many useful use cases for offering a tooltip on anchors, such as signalling that they are external, or that they will open in a new window, or to show preview information (think: preview windows on iOS Safari or the hovercards that display on GitHub over a user's handle).
It could be considered a mistake to allow title
on all elements; as adding
interactivity to non-interactive elements creates many problems. Limiting where
interesttarget
is allowed aims to create a "pit of success", guiding
developers to use it only on interactive elements, where it makes sense.
Whilst invoketarget
does replicate popovertarget
's functionality, it does
not necessarily mean popovertarget
gets removed from the spec.
invokeaction
is a freeform text hint to your own elements. If you feel it
necessary you can invent your own DSLs for passing extra data using this hint.
For example:
<button invoketarget="my-counter" invokeaction="add:1">Add 1</button>
<button invoketarget="my-counter" invokeaction="add:2">Add 2</button>
<button invoketarget="my-counter" invokeaction="add:10">Add 10</button>
<input readonly id="my-counter" value="0" />
<script>
const counter = document.getElementById("my-counter");
counter.addEventListener("invoke", (e) => {
let addMatch = /^add:(\d+)$/.match(e.action);
if (addMatch) {
counter.value = Number(counter.value) + Number(addMatch[1]);
}
});
</script>