iTranslated by AI
Causes and Solutions for Click Events Firing Twice When a <button> is Wrapped in a <label>
Hello everyone. Today, I'll explain an issue I recently encountered where a click event can fire twice when a <label> is wrapping a <button>.
First, please take a look at this CodePen.
Here, a button with "0" is displayed. This button is implemented so that clicking it once increases the number by 1.
However, clicking the number displayed on the button increases the number by 2. This is because the click event is firing twice. Clicking other parts (the edges of the button or the label) increases the number by 1.
The HTML and JavaScript implementations are as follows:
<p>Clicking the number triggers onClick twice!!!</p>
<div>
<label>
Label test
<button type="button"></button>
</label>
</div>
let count = 0;
const writeButtonContent = () => {
const button = document.querySelector('button');
const span = document.createElement('span');
span.textContent = String(count);
button.replaceChildren(span);
}
writeButtonContent();
document.querySelector('button').addEventListener('click', () => {
console.log('click');
count++;
writeButtonContent();
})
Anyone who can explain why this phenomenon occurs is quite familiar with HTML and the DOM. It took me a little while to understand it as well.
In this article, I will explain the cause of this phenomenon and introduce some countermeasures.
Preliminary Knowledge: About the label element
I'll briefly explain the label element. While the label element has roles related to accessibility, focusing strictly on behavior, clicking a label results in the associated form control being clicked. This is commonly used in combination with <input type="checkbox"> or <input type="radio">.
<label>
<input type="checkbox" name="lost"> Lost
</label>
By doing this, clicking the text "Lost" also counts as clicking the checkbox.
In the initial example, the label element wraps a button element. Since labels also support the button element, clicking the label counts as clicking the button element as well.
Causes of the Double Click Event
Now, let's get to the main topic.
In the initial example, the DOM structure after the JavaScript executes is as follows (the p and div elements are omitted as they are irrelevant here).
<label>
Label test
<button type="button"><span>0</span></button>
</label>

Initial DOM structure
The click event fires twice when the span element inside the button element is clicked. Upon debugging, it becomes clear that the first click event is fired on the span element, while the second click event is fired on the button element.
The key factors are event bubbling and the behavior of the label element. Additionally, the fact that DOM rewriting occurs during the click event in this sample is also relevant.
First, the first click event occurs at the span element (in other words, the event's target is the span element). Then, due to event bubbling, the click event handler is processed at its parent button element.

Event bubbling process
At this point, the DOM is updated as follows due to DOM rewriting:
<label>
Label test
<button type="button"><span>1</span></button>
</label>
Specifically, the content of the button element has become <span>1</span>, but this span element itself is newly created. In other words, the original <span>0</span> element has been removed from the DOM tree.
And the event processing continues.

DOM after rewriting and the event bubbling process
The behavior of the label element (when there is no for attribute) is that when the label element is clicked, it fires a click event on the form control [1] contained within it. This is the second click event fired on the button element.
However, simply wrapping a button element with a label element does not normally cause this issue. The fact that DOM rewriting was performed is one of the causes of this phenomenon.
In other words, when the label element processes the first click event, it likely determines, "If this click event was originally triggered by the button element being clicked, there's no need to fire an additional click event." Because of this, simply clicking a button element under normal circumstances won't fire the click event twice.
In this case, since the span element that triggered the first click has been removed from the DOM tree by this point, it is likely judged that the source of the trigger is not inside the button element. Consequently, the label element fires a second click event.
This is the reason why the button element's click event is processed twice just by clicking the button (its content) once. Rewriting the DOM in this way during a click event often happens when using a button element like a toggle button. I encountered this problem in a React application.
Checking the Specifications
This issue occurred because, during the label element's click processing, it was mistakenly determined that the trigger source was not the button element, causing the click event to be processed twice.
So, is there any basis for this behavior? The behavior of browsers for HTML documents is defined in the HTML Specification. Of course, there is a definition for the label element as well. Let's check the description in the specification.
Let's quote the part that gets straight to the heart of the matter.
The label element's exact default presentation and behavior, in particular what its activation behavior might be, if anything, should match the platform's label behavior. The activation behavior of a label element for events targeted at interactive content descendants of a label element, and any descendants of those interactive content descendants, must be to do nothing.
Consider the activation behavior mentioned here as the default behavior of an element defined by the specification. For example, navigating to a page when a link is clicked, or submitting a form when a <button type="submit"> is clicked, are examples of activation behaviors.
As you can see from reading this, the activation behavior of a label element is not defined as a specific algorithm. Instead, it is stated that it "should match the platform's label behavior." Regarding this, it is mentioned as an example:
For example, on platforms where clicking a label activates the form control, clicking the label in the following snippet could trigger the user agent to fire a click event at the input element, as if the element itself had been triggered by the user:
<label><input type=checkbox name=lost> Lost</label>
The behavior described in this example is the general behavior of labels on PCs and other devices. As far as I have confirmed, this is the behavior on both PCs and smartphones. However, this is not absolute; the specification mentions that other possible behaviors include "just gaining focus" or "nothing happening."
The issue here concerns the only part of the label element's activation behavior that was marked as "must":
The activation behavior of a label element for events targeted at interactive content descendants of a label element, and any descendants of those interactive content descendants, must be to do nothing.
Since a button element falls under interactive content, the case where "a span inside the button element is clicked" corresponds to this scenario. Therefore, under normal circumstances, the label element's activation behavior should do nothing.
The specification does not seem to explicitly state when this determination (whether the event target is interactive content) should be made.
While I haven't confirmed the browser implementation, I suspect the determination is made at the moment the activation behavior is triggered. As defined in the specification, the activation behavior is triggered after the entire event bubbling process is complete [2]. If this is the case, as mentioned above, the target span element has already been removed from the DOM tree at the time of determination, leading to the behavior where the activation behavior fires a click event for the button element.
However, since it is not currently strictly defined in the specification when to make the determination, a browser that correctly makes the determination even in the situation described in this article and does not fire the second click would not be in violation of the specification [3].
Countermeasures for the Double Click Event
Now that we understand the cause of the problem, let's look at some countermeasures. There are two main solutions.
Using preventDefault
Activation behavior can be canceled by preventDefault. Therefore, by calling preventDefault in the button element's click event handler, you can cancel the label element's activation behavior even if the button is wrapped in a label.
document.querySelector('button').addEventListener('click', (event) => {
+ event.preventDefault();
console.log('click');
count++;
writeButtonContent();
})
Note that you cannot stop this using stopPropagation instead of preventDefault. This is because stopPropagation prevents event listeners from being triggered during event bubbling, but it does not stop the activation behavior that occurs after the bubbling process is complete.
This method generally has fewer drawbacks and is a suitable countermeasure in most cases. In practice, if the button element is a component where you don't know if the parent is a label, you might unintentionally cancel the activation behavior of other parent elements. However, since the HTML specification prohibits nesting interactive content, this is unlikely to cause issues in practical use.
Using pointer-events: none;
Another countermeasure is to specify pointer-events: none; for the contents of the button.
button span {
pointer-events: none;
}
By doing this, the target of the click event will always be the button element, regardless of where on the button you click.
In our example, the issue was that the source of the event was removed from the DOM tree. Since the button element itself remains in the tree, the label element's activation behavior correctly identifies the trigger source, preventing the second click event from firing.
This is also a good approach, but care should be taken if the button is a component and its content is unknown, as descendant elements might override it with pointer-events: auto;.
Conclusion
In this article, I explained the issue where a click event fires twice when a <label> wraps a <button>.
We found that this occurs when the content of a button element is rewritten within its event handler, causing the source (target) element to be detached from the DOM tree. This is a common scenario in modern applications, such as those built with React.
I introduced two countermeasures: using preventDefault and using pointer-events: none;. You can choose the one that best fits your specific case.
If you have button components that update their content, try wrapping them in a <label>. You might be surprised to find that click events are firing twice!
-
More accurately, a labelable element ↩︎
-
Remember that calling
preventDefaultat any point during event bubbling can cancel the activation behavior. If so, it must occur after event bubbling is complete. ↩︎ -
However, if the behavior actually differed between browsers, I expect they would move toward unifying browser behavior and tightening the specification. ↩︎
Discussion
snapshot リンクだと次のページですね。