'Custom checkbox wont toggle when click and drag on label

I have a problem with my custom checkbox.

If you click on a checkbox element, move the mouse cursor and then release the click inside the checkbox area, the checkbox is checked.

However, if you do the same on a custom checkbox (here, a div inside a label), the checkbox isn't checked.

It's a problem because if you want to quickly check a checkbox, you may move the mouse after pressing the button of the mouse and before releasing it, thus not toggling the checkbox.

The user is obligated to click without moving the mouse.

I know I can use JS to emulate a checkbox with a div, but I want the HTML to be semantically correct, so: Is it possible to fix it without js?

Here's the code :

/* 1. Hide the checkbox */

      .hidden {
        /* https://zellwk.com/blog/hide-content-accessibly/ */
        border: 0;
        clip: rect(0, 0, 0, 0);
        height: auto;
        margin: 0;
        overflow: hidden;
        padding: 0;
        position: absolute;
        width: 1px;
        white-space: nowrap;
      }

/* 2. Use a label to retrieve the click event */

      label {
        /* not used directly to prevent the bug in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=608180 */
        pointer-events: none;
        display: inline-flex;
      }

      label > input {
        /*usefull for testing only*/
        pointer-events : all;
      }

      label > .customCheckbox {
        cursor: pointer;
        display: inline-flex;
        justify-content: center;
        align-items: center;
        position: relative;

        --size:200px;
        width: var(--size);
        height: var(--size);
        font-size: calc(var(--size)/2);
        pointer-events: all;
      }

      label > .customCheckbox::selection{
      /* prevent highliting the text within the custom checkbox */
          background: none;
      }

      label > .customCheckbox:after {
        
        z-index: -1;
        content: "✔";
        display: inline-flex;
        justify-content: center;
        align-items: center;

        position: absolute;
        width: 100%;
        height: 100%;
        
        box-sizing: border-box;
        border-radius: calc(var(--size)/4);
        border: solid black calc(var(--size)/10);
      }

      label > input:not(:checked) + div.customCheckbox:after {
        background:#0000;
        content: "\a0";
      }
<input type="checkbox"/>   

<label >
        <input type="checkbox" class="hidden"/>
        <div class="customCheckbox"></div>
</label >

I need the checkbox to be inside the label because I can't use the "for" attribute.

Thanks a lot for your help!

EDIT: for those wondering, here's the js solution (not ideal but since I can't do it in CSS, it's better than nothing) :

let checkboxes = document.getElementsByClassName("customCheckbox");

    for(let i=0; i<checkboxes.length; i++){
      checkboxes[i].addEventListener('mouseover', (e)=>{handleMouseOverCheckbox(e)})
      checkboxes[i].addEventListener('mousedown', (e)=>{handleMouseOverCheckbox(e)})
    }

    function handleMouseOverCheckbox(e) {
      e.srcElement.previousElementSibling.disabled = "true";
      if (e.buttons == 1) {
        e.srcElement.previousElementSibling.checked= !e.srcElement.previousElementSibling.checked;
      }}

EDIT 2 : Here's the best solution I could come up with, thanks to @zer00ne

codepen.io/DesignThinkerer/pen/bGVBLjM



Solution 1:[1]

A checkbox within a label is no problem. The problem arises when that checkbox is altered for the sake of accessibility instead of complete removal using display: none. When an interactive element like an input or button exists in the DOM, it will still be a factor no matter how its hidden unless display: none is applied.

In the Original Post the checkbox is almost impossible to click due to its 0px height and 1px width and yet when the div is clicked, the checkbox is clicked... sometimes not. Normally if the label was able to detect a click, that click would trigger a click event to the nested checkbox as well. In the OP code that's not happening because the label has pointer-events: none.

So the div is getting clicked and by some magical miracle this gains features that would not normally be attributed to it? Divs are not interactive they cannot affect elements that are not nested within themselves (i.e. like the checkbox that sits before the div). Nope, the div is useless, its the checkbox itself that's getting clicked due to the fact it is the only element within the inert label that gains focus by default. Gaining focus on an input doesn't necessarily guarantee a click event -- matter of fact a focus event selects an element and a click event sets an element as active. So what happens when a user double-clicks or moves the mouse quickly before the next click clears a label? Undesirable behavior as described in OP.

In the following demo, the checkboxes are hidden as per OP (also set width and height to 0) and removed pointer-events: none from the label and added it to the checkboxes. In this setup the label gains focus and click events and the click event will trigger the checkbox. The checkbox having been isolated from any extra clicks due to pointer-events: none and z-index: -1 should behave as expected.

As proof of concept I have added some JavaScript to demonstrate said code stability. The two event handlers are for demonstration purposes. The JS does not facilitate, stabilize, or modify performance of the HTML/CSS behavior.

  1. On any change event on a checkbox (via label) will trigger function changeHandler() to gather all the values of the checked checkboxes and display them in an output.

    • If there's a checkmark in a box and there's a value displayed that corresponds to said checked checkbox, then it successfully passes as valid behavior.
  2. Clicking button.show will trigger function clickHandler() to toggle the .reveal class to each checkbox.

    • While clicking rapidly observe that the revealed checkboxes are checked and its corresponding custom label is checked as well. Also notice that the value should also be displayed as well.

BTW

  • "....target doesn't work in IE IIRC"

    event.target is the standard property to use in every modern browser. event.srcElement is a deprecated property used by IE which is almost entirely unsupported.

  • pointer-events: all assigned to input and .customCheckbox

    The value all applies to SVG only. Only the values of none and auto are relevant to HTML. auto is default.


Demo

I cannot reproduce the described behavior except in the code provided in OP. If you can reproduce that behavior on my demo, please record a short video of it and post that and the machine/device, OS, and browsers (I will assume everything is reasonably up to date).

const main = document.forms.main;

main.onchange = checkHandler;

function checkHandler(e) {
  const fc = this.elements;
  const chx = [...fc.hidden];
  const ui = e.target;

  if (ui.matches('.hidden')) {
    let text = chx.flatMap(c => c.checked ? [c.value] : []);
    fc.view.value = '';
    fc.view.value = text.join(', ');
  }
}

main.onclick = clickHandler

function clickHandler(e) {
  const fc = this.elements;
  const chx = [...fc.hidden];
  const ui = e.target;

  if (ui.matches('button.show')) {
    chx.forEach(c => c.classList.toggle('reveal'));
  }
}
:root,
body {
  --size: 10rem;
  font: 400 small-caps 2vw/1.5 Times;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}

*,
*::before,
*::after {
  box-sizing: inherit;
  font: inherit;
}

.display {
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  align-items: center;
  width: max-content;
  max-height: min-content;
  margin: 10px;
  border: calc(var(--size) / 20) solid #000;
  border-radius: 24px;
}

.view {
  display: inline-block;
  min-width: 35ch;
  font-size: 1.5rem;
  height: 1.5rem;
  line-height: 1;
}

.show {
  display: inline-block;
  width: 12ch;
  padding: 1px 3px;
  margin: 4px;
  border: 2px solid #000;
  border-radius: 8px;
  background: none;
  text-align: center;
  font-size: 1.25rem;
  cursor: pointer;
}

.mask {
  position: relative;
  z-index: 2;
  display: block;
  width: var(--size);
  height: var(--size);
  padding: 0;
  margin: 0 5px;
  border: solid black calc(var(--size) / 10);
  border-radius: calc(var(--size) / 4);
  font-size: calc(var(--size) * 0.8);
  background: none;
  cursor: pointer;
}

.icon {
  position: absolute;
  z-index: 1;
  top: -1.5rem;
  right: 1rem;
  display: inline-block;
  margin: 0;
  padding: 0;
  border: 0;
}

.hidden {
  position: relative;
  z-index: -1;
  display: inline-block;
  width: 0;
  height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
}

.reveal {
  z-index: 0;
  top: -24px;
  left: 4px;
  width: 1px;
  height: 1px;
  opacity: 1;
}

.icon::after {
  content: attr(data-blank);
}

.hidden:checked+.icon::after {
  content: attr(data-check);
}
<!DOCTYPE html>
<html>

<head lang='en'>
  <meta charset='utf-8'>
  <style></style>
</head>

<body>
  <form name='main'>
    <fieldset name='display' class='display'>
      <output name='view' class='view'></output>
      <button name='show' class='show' type='button'>Show</button>
    </fieldset>
    <fieldset name='display' class='display'>
      <label name='mask' class='mask'>
    <input name='hidden' class="hidden" type="checkbox" value='Check I'>
    <fieldset name='icon' class='icon' data-check='?' data-blank=' '></fieldset>
  </label>


      <label name='mask' class='mask'>
    <input name='hidden' class="hidden" type="checkbox" value='Check II'>
    <fieldset name='icon' class='icon' data-check='?' data-blank=' '></fieldset>
  </label>


      <label name='mask' class='mask'>
    <input name='hidden' class="hidden" type="checkbox" value='Check III'>
    <fieldset name='icon' class='icon' data-check='?' data-blank=' '></fieldset>
  </label>


      <label name='mask' class='mask'>
    <input name='hidden' class="hidden" type="checkbox" value='Check IV'>
    <fieldset name='icon' class='icon' data-check='?' data-blank=' '></fieldset>
  </label>
    </fieldset>
  </form>

  <script></script>
</body>

</html>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 zer00ne