:indeterminate, When a Form Control is Confused
It's a semi-state given to checkboxes under a specific circumstance in which there is a "parent" or "primary" ("boss"?) checkbox that can un/check all subordinate checkboxes. We know that if the primary checkbox is checked then all of the subordinates are checked and they become all unchecked when the primary is unchecked. But what if the user checks or unchecks one of the subordinates instead? The primary shouldn't uncheck itself because whatever state it changes to should always dictate what the subordinates will do. So that's when the indeterminate should apply.
The checkbox in indeterminate state seems to be untouchable so if you want to directly style it, you'll need to hide the real checkbox and style an associated label as the surrogate:
HTML
<!-- You won't see indeterminate attribute displayed in devtool -->
<input id="MAJOR" type="checkbox" indeterminate>
<label for="MAJOR">❎</label>
<input id="MINORa" type="checkbox" checked>
<label for="MINORa">✅</label>
<input id="MINORb" type="checkbox">
<label for="MINORb"></label>
CSS
[type="checkbox"] {
display: none;
}
label::before {
content: '';
}
[type="checkbox"]:checked + label::before {
content: '✅';
}
[type="checkbox"]:indeterminate + label::before {
content: '❎';
}
This old hack works great, but if you have to consider accessibility, you'll need to resort to plugins, a framework, a library, etc.. Or if you are willing to put in the research and effort it is possible to use the hack and maintain accessibility.
In the following example, Font-Awesome 6 is used to replace the checkboxes. Styling them is actually easier than native elements.
Details are commented in example
// Register form#root to the "change" event
document.forms.root.onchange = status;
function status(event) {
// Reference the element user un/checked
const pick = event.target;
// Reference all form controls (fieldsets and inputs)
const tree = this.elements;
let trunk, branch, leaves;
/*
If pick has class .leaf...
...find the primary checkbox it belongs to...
...find the fieldset it belongs to...
...make an array of all checkboxes in the fieldset...
...find the total amount of checked boxes
*/
/*
If no boxes are checked, uncheck primary...
...if all checkboxes are checked check primary...
...otherwise uncheck primary and set it to indeterminate
*/
/*
If the checkbox the user picked was a "trunk"...
...make an array of all of it's subordinate checkboxes...
...if primary is checked check all of the checkboxes and
if unchecked, theen all subordinates will be unchecked as well
*/
if (pick.matches(".leaf")) {
trunk = tree[pick.name.toUpperCase()];
branch = tree[pick.name + "X"];
leaves = [...branch.elements];
let checked = leaves.filter(
(leaf) => leaf.checked).length;
let state =
!checked ? [false, false] :
checked === leaves.length ?
[true, false] :
[false, true];
trunk.checked = state[0];
trunk.indeterminate = state[1];
}
if (pick.name === "trunk") {
leaves = [...tree[pick.id.toLowerCase()]];
let checkAll = pick.checked;
leaves.forEach((leaf) => (leaf.checked = checkAll));
}
}
@import url("https://fonts.googleapis.com/css2?family=Raleway:wght@500&display=swap");
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font: 2ch/1.2 Consolas;
}
html,
body {
width: 100%;
min-height: 100%;
}
form {
margin: 0 auto;
padding: 5vh;
}
header {
margin-bottom: 0.75rem;
}
h1 {
margin-bottom: 0.5rem;
font-family: Raleway;
font-size: 1.25rem;
}
[name="trunk"],
.leaf {
display: none;
}
[name="branch"] {
width: max-content;
margin: -0.5rem 1rem 1rem 0.75rem;
padding: 0 1.5em;
}
legend {
margin-bottom: 1rem;
}
menu {
display: flex;
flex-flow: column nowrap;
list-style: none;
margin-top: 0;
padding-left: 0;
}
label {
display: block;
margin-bottom: 1rem;
}
menu label:last-of-type {
margin-bottom: 0.25rem;
}
code {
font-family: Consolas;
}
.far {
font: var(--fa-font-regular);
}
/* Replace .far with .fas for inversed versions of the icons */
.fas {
font: var(--fa-font-solid);
}
.far::before {
content: "\f0c8";
color: blue;
}
.leaf:checked + .far::before,
[name="trunk"]:checked + .far::before {
content: "\f14a";
}
[name="trunk"]:indeterminate + .far::before {
content: "\f146";
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" rel="stylesheet">
<form id="root" name="tree">
<header>
<h1>Indeterminate Checkboxes </h1>
<code>id="root" name="tree" ⽊</code>
</header>
<input id="A" name="trunk" type="checkbox">
<label for="A" class="far">
<code> id="A" name="trunk" ⽙</code>
</label>
<fieldset id="aX" name="branch">
<legend>
<code> id="aX" name="branch" ⽀</code>
</legend>
<menu>
<input id="a1" name="a" class="leaf" type="checkbox">
<label for="a1" class="far">
<code> id="a1" name="a" class="leaf" ⾴</code>
</label>
<input id="a2" name="a" class="leaf" type="checkbox">
<label for="a2" class="far">
<code> id="a2" name="a" class="leaf" ⾴</code>
</label>
<input id="a3" name="a" class="leaf" type="checkbox">
<label for="a3" class="far">
<code> id="a3" name="a" class="leaf" ⾴</code>
</label>
</menu>
</fieldset>
<input id="B" name="trunk" type="checkbox">
<label for="B" class="far">
<code> id="B" name="trunk" ⽙</code>
</label>
<fieldset id="bX" name="branch">
<legend>
<code> id="bX" name="branch" ⽀</code>
</legend>
<menu>
<input id="b1" name="b" class="leaf" type="checkbox">
<label for="b1" class="far">
<code> id="b1" name="b" class="leaf" ⾴</code>
</label>
<input id="b2" name="b" class="leaf" type="checkbox">
<label for="b2" class="far">
<code> id="b2" name="b" class="leaf" ⾴</code>
</label>
<input id="b3" name="b" class="leaf" type="checkbox">
<label for="b3" class="far">
<code> id="b3" name="b" class="leaf" ⾴</code>
</label>
</menu>
</fieldset>
</form>