data-toggle-tags
Style checkboxes as selectable pill chips for tag-based filtering and multi-select. Pure CSS with optional JavaScript for max selection limits.
Overview
The data-toggle-tags attribute transforms a fieldset of checkboxes into selectable pill-shaped tags. The styling is pure CSS via :has(:checked) — JavaScript is only loaded when you need a max selection limit via data-max.
<fieldset data-toggle-tags>\n <legend>Topics</legend>\n <label><input type="checkbox" name="topics" value="ai"> AI / ML</label>\n <label><input type="checkbox" name="topics" value="web"> Web Dev</label>\n <label><input type="checkbox" name="topics" value="mobile"> Mobile</label>\n</fieldset>
How It Works
Add data-toggle-tags to a <fieldset> containing checkbox inputs inside labels. The mechanism is primarily CSS:
- Each
<label>is styled as a pill chip with padding, border, and rounded corners - The checkbox inside each label is visually hidden with
opacity: 0 - CSS
:has(:checked)detects when a checkbox is selected and applies the active style - Clicking the label toggles the hidden checkbox, which toggles the visual state
- No JavaScript is needed for the basic toggle behavior
When data-max is set, a small JavaScript module is loaded. It listens for checkbox changes and disables unchecked checkboxes once the limit is reached, re-enabling them when selections are removed.
Attributes
| Attribute | Type | Description |
|---|---|---|
data-toggle-tags |
boolean | Placed on a <fieldset>. Enables pill chip styling for child checkbox labels. |
data-max |
number | Maximum number of selections allowed. When reached, unchecked checkboxes are disabled. Requires JavaScript. |
Basic Tags
Without data-max, toggle tags are entirely CSS-driven. Users can select and deselect any number of tags freely.
<fieldset data-toggle-tags>\n <legend>Topics</legend>\n <label><input type="checkbox" name="topics" value="ai"> AI / ML</label>\n <label><input type="checkbox" name="topics" value="web"> Web Dev</label>\n <label><input type="checkbox" name="topics" value="mobile"> Mobile</label>\n</fieldset>
Max Selection Limit
Add data-max to cap the number of selections. When the limit is reached, remaining unchecked tags are disabled and visually muted. Deselecting a tag re-enables the others.
<fieldset data-toggle-tags data-max="3">\n <legend>Pick up to 3</legend>\n <label><input type="checkbox" name="skills" value="js"> JavaScript</label>\n <label><input type="checkbox" name="skills" value="py"> Python</label>\n <label><input type="checkbox" name="skills" value="go"> Go</label>\n <label><input type="checkbox" name="skills" value="rust"> Rust</label>\n <label><input type="checkbox" name="skills" value="ts"> TypeScript</label>\n <label><input type="checkbox" name="skills" value="ruby"> Ruby</label>\n</fieldset>
Pre-checked Tags
Add the native checked attribute to pre-select tags on load. This works with or without data-max.
<fieldset data-toggle-tags>\n <legend>Interests</legend>\n <label><input type="checkbox" name="interest" value="music" checked> Music</label>\n <label><input type="checkbox" name="interest" value="film"> Film</label>\n <label><input type="checkbox" name="interest" value="books" checked> Books</label>\n <label><input type="checkbox" name="interest" value="sports"> Sports</label>\n</fieldset>
Filter UI Pattern
Toggle tags work naturally in filter forms. The checkboxes submit as standard form values, making server-side filtering straightforward.
<form> <fieldset data-toggle-tags> <legend>Filter by category</legend> <label><input type="checkbox" name="cat" value="design"> Design</label> <label><input type="checkbox" name="cat" value="dev"> Development</label> <label><input type="checkbox" name="cat" value="marketing"> Marketing</label> <label><input type="checkbox" name="cat" value="product"> Product</label> </fieldset> <button type="submit">Apply filters</button></form>
Form Participation
Because toggle tags use real checkboxes, they participate fully in form behavior without JavaScript:
- Submission — checked tags submit as
name=valuepairs - Reset —
<button type="reset">restores original checked states - Validation — native
requiredworks on individual checkboxes - FormData —
new FormData(form)includes all selected tags
Styling
The pill chip appearance is driven entirely by CSS. The :has(:checked) selector eliminates the need for JavaScript class toggling.
/* Tags use CSS :has() — no JS needed for styling */[data-toggle-tags] label { display: inline-flex; align-items: center; padding: var(--size-2xs) var(--size-s); border: 1px solid var(--color-border); border-radius: var(--radius-pill); cursor: pointer; transition: background-color 0.15s, border-color 0.15s;} /* Checked state via :has() */[data-toggle-tags] label:has(:checked) { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary);} /* Hide the checkbox visually */[data-toggle-tags] input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0;} /* Disabled state when max reached */[data-toggle-tags] label:has(:disabled:not(:checked)) { opacity: 0.5; cursor: not-allowed;}
The checked state swaps the background to --color-primary and the text to --color-on-primary. Disabled tags (from data-max) use reduced opacity.
Dynamic Elements
Fieldsets added to the DOM after page load are automatically enhanced via a MutationObserver when data-max is present. No manual initialization is needed. Tags without data-max require no JavaScript at all.
Pick up to 2 <label><input type="checkbox" name="opt" value="a"> Option A</label> <label><input type="checkbox" name="opt" value="b"> Option B</label> <label><input type="checkbox" name="opt" value="c"> Option C</label>`;document.body.appendChild(fieldset);// fieldset is ready — no manual init needed/code-block
Accessibility
- Native
<input type="checkbox">elements provide full keyboard support — Tab to navigate, Space to toggle - The
<fieldset>and<legend>provide a group label announced by screen readers - Checked/unchecked state is conveyed natively by the checkbox semantics
- Disabled state (from
data-max) is announced via the nativedisabledattribute - Form reset restores original checked states, including visual styling
- Focus indicators use
:focus-visibleon the label for clear keyboard navigation - Without JavaScript, checkboxes render as standard checkboxes with label text (progressive enhancement)