I will have 2 lists: with radio buttons and with checkboxes (multi-level) My current html is:
<label>
<input type="radio" name="group" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>
<label>
<input type="radio" name="subgroup" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>
<label>
<input type="radio" name="subgroup" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>
Is it fine that I don't have my subgroup
inside of first label? I've seen many different examples, but I was wondering which one is semantically correct?
To answer your first question, <label>
elements can only label one form element at a time, so you shouldn't put more than one form element inside a <label>
.
For the radio group, since you can only select one sub-item out of all of the sub-items, the most semantic and accessible way to present this would be as a <select>
element with <optgroup>
elements for each top-level grouping and <option>
elements for each sub-item. That's what this element is for.
<label for="dino-select">Choose a dinosaur:</label>
<select id="dino-select">
<optgroup label="Theropods">
<option>Tyrannosaurus</option>
<option>Velociraptor</option>
<option>Deinonychus</option>
</optgroup>
<optgroup label="Sauropods">
<option>Diplodocus</option>
<option>Saltasaurus</option>
<option>Apatosaurus</option>
</optgroup>
</select>
For the case where you need to be able to select multiple things, you can add the multiple
attribute:
<label for="dino-select">Choose one or more dinosaurs:</label>
<select id="dino-select" multiple>
<optgroup label="Theropods">
<option>Tyrannosaurus</option>
<option>Velociraptor</option>
<option>Deinonychus</option>
</optgroup>
<optgroup label="Sauropods">
<option>Diplodocus</option>
<option>Saltasaurus</option>
<option>Apatosaurus</option>
</optgroup>
</select>
If you don't like using a <select multiple />
element (some users don't know how to use them because they're uncommon) you could use a nested list of checkboxes.
Be sure to semantically denote which items are owned by other items in an accessible way, and include javascript to add the necessary functionality.
Notes on expected behavior:
Here is one approach to that:
const setInputState = (el, state) => {
if (state === 'indeterminate') {
el.indeterminate = true
} else {
el.indeterminate = false
el.checked = state
}
}
const updateOwned = (el) => {
if (el.hasAttribute('data-children')) {
let state = el.checked
el.getAttribute('data-children').split(' ').forEach(id => {
let owned = document.getElementById(id)
setInputState(owned, state)
updateOwned(owned)
})
}
}
const updateOwner = (el) => {
if (el.hasAttribute('data-parent')) {
let owner = document.getElementById(el.getAttribute('data-parent'))
let states = []
let collectiveState
owner.getAttribute('data-children').split(' ').every(id => {
let owned = document.getElementById(id)
let state = owned.indeterminate === true ? 'indeterminate' : owned.checked
if (states.length > 0 && states.indexOf(state) === -1) {
collectiveState = 'indeterminate'
return false
} else {
states.push(state)
return true
}
})
collectiveState = collectiveState || states[0]
setInputState(owner, collectiveState)
updateOwner(owner)
}
}
document.querySelectorAll('.nested-multiselect').forEach(multiselect => {
multiselect.querySelectorAll('input[type="checkbox"][data-children], input[type="checkbox"][data-parent]').forEach(input => {
input.addEventListener('change', event => {
updateOwned(event.currentTarget)
updateOwner(event.currentTarget)
})
})
})
body {
padding: 2rem;
}
label {
display: block;
}
label span:before {
content: ' ';
}
fieldset fieldset {
border: none;
padding: 0 0 0 1ch;
}
<fieldset class="nested-multiselect">
<legend>Categories </legend>
<label id="label-fruit">
<input id="fruit" type="checkbox" name="categories" value="fruit" aria-owns="subcategories-fruit" data-children="apple orange banana"/><span>fruit</span>
</label>
<fieldset id="subcategories-fruit" aria-label="fruit subcategories">
<label id="label-apple">
<input id="apple" type="checkbox" name="categories" value="apple" aria-owns="subcategories-apple" data-parent="fruit" data-children="gala macintosh honeycrisp"/><span>apple</span>
</label>
<fieldset id="subcategories-apple" aria-label="apple subcategories">
<label id="label-gala">
<input id="gala" type="checkbox" name="categories" value="gala" data-parent="apple"/><span>gala</span>
</label>
<label id="label-macintosh">
<input id="macintosh" type="checkbox" name="categories" value="macintosh" data-parent="apple"/><span>macintosh</span>
</label>
<label id="label-honeycrisp">
<input id="honeycrisp" type="checkbox" name="categories" value="honeycrisp" data-parent="apple"/><span>honeycrisp</span>
</label>
</fieldset>
<label id="label-orange">
<input id="orange" type="checkbox" name="categories" value="orange" data-parent="fruit"/><span>orange</span>
</label>
<label id="label-banana">
<input id="banana" type="checkbox" name="categories" value="banana" data-parent="fruit"/><span>banana</span>
</label>
</fieldset>
<label id="label-vegetables">
<input id="vegetables" type="checkbox" name="categories" value="vegetables" aria-owns="subcategories-vegetables" data-children="squash peas leek"/><span>vegetables</span>
</label>
<fieldset id="subcategories-vegetables" aria-label="vegetables subcategories">
<label id="label-squash">
<input id="squash" type="checkbox" name="categories" value="squash" data-parent="vegetables"/><span>squash</span>
</label>
<label id="label-peas">
<input id="peas" type="checkbox" name="categories" value="peas" data-parent="vegetables"/><span>peas</span>
</label>
<label id="label-leek">
<input id="leek" type="checkbox" name="categories" value="leek" data-parent="vegetables"/><span>leek</span>
</label>
</fieldset>
</fieldset>