Skip to content

Instantly share code, notes, and snippets.

@stamminator
Created December 19, 2024 02:30
Show Gist options
  • Save stamminator/98da95eb34b50d8d1cfee56d61abfe5e to your computer and use it in GitHub Desktop.
Save stamminator/98da95eb34b50d8d1cfee56d61abfe5e to your computer and use it in GitHub Desktop.
Demo of a simple two-state toggle button
:root {
--default-text: "State 1 (root)";
--toggled-text: "State 2 (root)";
}
button {
text-align: center;
vertical-align: middle;
}
.btn {
display: inline-block;
border: 2px outset transparent;
padding: 6px 12px;
font-family: sans-serif;
border-radius: 2px;
color: #fff;
background-color: #474141;
border-color: #3a3636;
&:hover {
background-color: #3a3636;
}
&:active {
border-style: inset;
background-color: #2d2828;
}
}
.btn-toggle {
display: inline-flex;
flex-wrap: wrap;
width: min-content;
> * {
flex-grow: 1;
white-space: nowrap;
overflow-y: hidden;
}
> :first-child {
height: auto;
}
> :nth-child(2) {
height: 0;
}
&.toggled {
flex-wrap: wrap-reverse; /* preserves "first baseline" behavior */
> :first-child {
height: 0;
}
> :last-child {
height: auto;
}
}
}
.btn-toggle-css {
display: inline-flex;
flex-wrap: wrap;
width: min-content;
> * {
display: none; /* ensure only pseudoelements appear */
}
&::before,
&::after {
flex-grow: 1;
white-space: nowrap;
overflow-y: hidden;
}
&::before {
height: auto;
content: var(--default-text);
}
&::after {
height: 0;
content: var(--toggled-text);
}
&.toggled {
flex-wrap: wrap-reverse; /* preserves "first baseline" behavior */
&::before {
height: 0;
}
&::after {
height: auto;
}
}
}
<div style="display: inline-grid; grid-template-columns: auto auto; gap: 6px; align-items: first baseline;">
<div>Simple toggle</div>
<button type="button" class="btn btn-toggle" onclick="this.classList.toggle('toggled')">
<span>Untoggled state</span>
<span>Toggled state</span>
</button>
<div>Pure CSS</div>
<button type="button" class="btn btn-toggle-css" onclick="this.classList.toggle('toggled')"></button>
<div>Pure CSS with overrides</div>
<button type="button" class="btn btn-toggle-css" onclick="this.classList.toggle('toggled')"
style="--toggled-text: 'State 2 (element)'">
<span>oops</span>
</button>
<div>Toggle with accessibility</div>
<div>
<button type="button" class="btn btn-toggle" onclick="toggleButton(this)">
<span>Accessible label (default)</span>
<span>Accessible label (toggled)</span>
</button>
</div>
<div>Advanced toggle<br/>(set state externally)</div>
<div>
<button id="advancedToggleButton" type="button" class="btn btn-toggle" onclick="toggleButton(this, selectOrUnselectAll)">
<span>Select all</span>
<span>Unselect all</span>
</button>
<div>
<label><input type="checkbox" name="checkboxes" value="1" checked /> Box 1</label>
<label><input type="checkbox" name="checkboxes" value="2" /> Box 2</label>
<label><input type="checkbox" name="checkboxes" value="3" /> Box 3</label>
</div>
</div>
</div>
'use strict'
/**
@param {HTMLButtonElement} btnEl
@param {function(HTMLButtonElement=):boolean=} toggledStateFn
If provided, rather than the button's state being reversed, it will
be set to the result of this function regardless of its current state.
*/
function toggleButton(btnEl, toggledStateFn) {
/**@type {boolean} */ let isNewStateToggled;
if (toggledStateFn)
isNewStateToggled = toggledStateFn(btnEl);
else
isNewStateToggled = !btnEl.classList.contains('toggled');
if (isNewStateToggled) {
btnEl.classList.add('toggled');
btnEl.firstElementChild.setAttribute('aria-hidden', 'true');
btnEl.lastElementChild.setAttribute('aria-hidden', 'false');
}
else {
btnEl.classList.remove('toggled');
btnEl.firstElementChild.setAttribute('aria-hidden', 'false');
btnEl.lastElementChild.setAttribute('aria-hidden', 'true');
}
}
function synchronizeAdvancedToggleButtonState() {
toggleButton(document.getElementById("advancedToggleButton"), () => {
// Are all checkboxes checked?
let checkboxes = document.getElementsByName("checkboxes");
let allChecked = Array.from(checkboxes).every(x => x.checked === true);
return allChecked;
});
}
/** @param {HTMLButtonElement} btnEl */
function selectOrUnselectAll(btnEl) {
let isCurrentStateToggled = btnEl.classList.contains('toggled');
Array.from(document.getElementsByName("checkboxes")).forEach(x => x.checked = !isCurrentStateToggled);
return !isCurrentStateToggled;
}
document.getElementsByName("checkboxes").forEach(x => {
x.addEventListener('change', synchronizeAdvancedToggleButtonState);
});
// Initialize advancedToggleButton's state so it's in sync with the checkboxes
// and so that we don't have to wait until it's pressed to set the ARIA attributes.
synchronizeAdvancedToggleButtonState();
name: Two-state Toggle Button
description: Demo of a two-state toggle button that maintains the same width even as the inner text changes, without using JavaScript or hard-coded widths. With a bit of flexbox magic, the width of the button is derived from whichever of the two states' text is wider.
authors:
- Jacob Stamm
resources:
normalize_css: no
wrap: l
panel_js: 0
panel_css: 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment