Fylgja Multiselect

A multiselect lets the reader pick any number of options from a short list, such as notification types or topics to filter by. Instead of a <select multiple>, it is a <fieldset> of checkboxes: every option is visible, each one toggles with a click or Space, and the values submit with the form like any other checkbox. The focusgroup attribute turns the group into a single Tab stop with arrow key navigation, so a keyboard user does not have to tab through every option.

Preview

Notify me about
<fieldset class="multiselect" focusgroup="toolbar block wrap">
	<legend>Notify me about</legend>
	<div class="multiselect-options divide-y rounded clip" style="--my: 0.5rem 0">
		<label class="flex gap align">Comments <input type="checkbox" name="notify" value="comments" checked /></label>
		<label class="flex gap align">Mentions <input type="checkbox" name="notify" value="mentions" checked /></label>
		<label class="flex gap align">New releases <input type="checkbox" name="notify" value="releases" /></label>
		<label class="flex gap align">Newsletter <input type="checkbox" name="notify" value="newsletter" /></label>
	</div>
</fieldset>

<style>
	.multiselect {
		.multiselect-options {
			border: 1px solid color-mix(in oklab, var(--root-bg) 75%, var(--root-fg));
		}

		label {
			padding: 0.6em 0.9em;
		}

		label:has(:checked) {
			background: color-mix(in oklab, var(--root-bg) 92%, var(--root-fg));
		}

		input:focus-visible {
			outline: none;
		}

		label:has(:focus-visible) {
			outline: 2px solid;
			outline-offset: -2px;
		}
	}
</style>

Usage

  1. Wrap the options in a <fieldset> and name the group with a <legend>.
  2. Give every checkbox the same name and its own value. The form submits one entry per checked option.
  3. Wrap each checkbox in its <label>, so the whole row is clickable and no id is needed. flex gap align puts the text at the start and the checkbox at the end of the row.
  4. Add focusgroup="toolbar block wrap" to the fieldset. block matches the arrow keys to a vertical list (Up and Down), and wrap moves from the last option back to the first.

A fieldset keeps its own group role, so the toolbar token only adds the arrow key behavior and does not change how the group is announced.

Anatomy

Part Responsibility
<fieldset> Groups the options and holds the focusgroup attribute.
<legend> Names the group, announced when focus enters it.
.multiselect-options The frame, with divide-y for the row dividers. Kept off the fieldset so the legend sits above it.
<label> One option row, clickable as a whole and styled from its checkbox.
<input type=checkbox> The option’s state and submitted value.
focusgroup One Tab stop for the group, arrow keys between options.

Accessibility

  • The <legend> is announced as the group name when focus moves into it, and each checkbox keeps its native checked state and label.
  • With focusgroup, Tab enters and leaves the group in one step, the arrow keys move between options, Home and End jump to the first and last, and Space toggles. Returning to the group focuses the option you left.
  • The focus ring moves from the small checkbox to the whole row (label:has(:focus-visible)), so the focused option is easy to spot while arrowing through the list.
  • Checked rows change their background as well as the checkmark, so the state does not rely on one small icon.

Variants

Chips

The same checkboxes laid out as a wrapping row of chips, a compact choice for tags or filters. Each chip is a .btn label, so hover and the checked state come from the button styles: .btn treats :has(input:checked) as active, and the --btn-active-* variables color it. The checkbox shrinks away until it is checked, so a checked chip shows both a checkmark and the brand color. Without block the arrow keys follow the row, Left and Right.

Topics
<fieldset class="multiselect-chips" focusgroup="toolbar wrap">
	<legend>Topics</legend>
	<div class="flex-wrap gap-sm" style="--my: 0.5rem 0">
		<label class="btn"><input type="checkbox" name="topic" value="css" checked /> CSS</label>
		<label class="btn"><input type="checkbox" name="topic" value="html" /> HTML</label>
		<label class="btn"><input type="checkbox" name="topic" value="a11y" checked /> Accessibility</label>
		<label class="btn"><input type="checkbox" name="topic" value="design" /> Design systems</label>
		<label class="btn"><input type="checkbox" name="topic" value="perf" /> Performance</label>
	</div>
</fieldset>

<style>
	.multiselect-chips {
		--btn-py: 0.2rem;
		--btn-radius: 5rem;
		--btn-border-width: 1px;
		--btn-stroke: color-mix(in oklab, var(--root-bg) 70%, var(--root-fg));
		--btn-active-stroke: var(--brand);
		--btn-active-bg: var(--brand);
		--btn-active-color: var(--on-brand);

		input {
			--control-size: 1em;
			--control-border-width: 0;
			--control-checked-bg: transparent;
			--control-checked-color: currentcolor;
			--form-bg: transparent;
			inline-size: 0;
			padding: 0;
			margin-inline-end: -0.5em;
			transition: inline-size 0.15s, margin 0.15s;
		}

		input:checked {
			inline-size: var(--control-size);
			margin-inline-end: 0;
		}

		input:focus-visible {
			outline: none;
		}

		label:has(:focus-visible) {
			outline: 2px solid var(--root-fg);
			outline-offset: 2px;
		}
	}
</style>

Browser support

focusgroup is supported natively from Chrome and Edge 150. Other browsers ignore the attribute, and every checkbox stays a normal Tab stop, which is a safe fallback. The attribute is still marked experimental, so check Can I use before relying on it.

To get the arrow keys everywhere, load the focusgroup polyfill once per page. It does nothing in browsers that support focusgroup natively.

<script type="module">
	import { polyfill } from "https://cdn.jsdelivr.net/npm/@microsoft/focusgroup-polyfill@1/build/index-shadowless.min.mjs";

	polyfill();
</script>

Related: Input Switch, Select, Form Field.

FAQ

Why not use select with the multiple attribute?

A multiple select needs Ctrl or Cmd click to pick more than one option, which most people do not know, and on desktop it renders as a cramped list box. Checkboxes show every option and its state at a glance and work the same with mouse, touch, and keyboard.

Why is a multiple select hard to use with a keyboard?

In a multiple select a plain arrow key press moves the selection, so it silently clears every option picked before. Keeping them means holding a modifier, Shift plus an arrow for a range or Ctrl plus the arrows and Space for separate options, and those shortcuts differ per browser and operating system and are not shown anywhere. One wrong key press undoes the work. A checkbox only changes when you press Space on it, so moving through the list never loses a choice.

Can an option show more than text, like a price or an icon?

Yes, that is another reason to use checkboxes. A customizable select can hold rich option content, but only as a single select. A multiple select still only renders what the option element allows, which is plain text. Each option here is a label, so it can hold any inline HTML, such as an icon, a price, a short description, or a badge, and you style it like any other markup. Keep the label text meaningful on its own, since all of it becomes the checkbox's accessible name.

What does focusgroup add?

Without it every checkbox is a separate Tab stop, so a long list costs many key presses to get past. With focusgroup the whole group is one Tab stop and the arrow keys move between options, while Space still toggles the focused one.

What happens in a browser without focusgroup?

The attribute is ignored and every checkbox stays a normal Tab stop. The component keeps working, it only loses the arrow keys. Load the polyfill from the Browser support section to get them everywhere.

How many options is too many?

Checkboxes suit a short list that fits on screen. When the list grows past a dozen or so, add a search field above it or split the options into groups, each with its own fieldset and legend.