@use 'sprucecss/scss/spruce' as *;
.range-group {
display: flex;
flex-direction: column;
gap: spacer('xs');
+ .range-group {
margin-block-start: spacer('l');
}
&__label {
align-items: baseline;
display: flex;
flex-wrap: wrap;
gap: spacer('xs');
justify-content: space-between;
}
&__value {
color: color('primary');
font-variant-numeric: tabular-nums;
font-weight: 700;
}
&__inner {
align-items: center;
display: flex;
gap: spacer('xs');
.form-range {
flex-grow: 1;
margin-block-start: 0;
}
.btn {
flex-shrink: 0;
}
}
}
<div class="range-group" data-range-group data-unit-one="seat" data-unit-many="seats">
<div class="range-group__label">
<label class="form-label" for="seats">Seats</label>
<output class="range-group__value" for="seats" aria-hidden="true"></output>
</div>
<div class="range-group__inner">
<button class="btn btn--primary btn--sm btn--icon" type="button" aria-controls="seats" data-step="-1">
<span class="sr-only">Decrease seats</span>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 12h14"/></svg>
</button>
<input class="form-range range-group__control" id="seats" type="range" min="1" max="50" value="5" step="1">
<button class="btn btn--primary btn--sm btn--icon" type="button" aria-controls="seats" data-step="1">
<span class="sr-only">Increase seats</span>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M5 12h14"/></svg>
</button>
</div>
</div>
<div class="range-group" data-range-group data-unit-one="GB" data-unit-many="GB">
<div class="range-group__label">
<label class="form-label" for="storage">Storage</label>
<output class="range-group__value" for="storage" aria-hidden="true"></output>
</div>
<div class="range-group__inner">
<button class="btn btn--primary btn--sm btn--icon" type="button" aria-controls="storage" data-step="-1">
<span class="sr-only">Decrease storage</span>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 12h14"/></svg>
</button>
<input class="form-range range-group__control" id="storage" type="range" min="10" max="500" value="100" step="10">
<button class="btn btn--primary btn--sm btn--icon" type="button" aria-controls="storage" data-step="1">
<span class="sr-only">Increase storage</span>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M5 12h14"/></svg>
</button>
</div>
</div>
document.querySelectorAll('[data-range-group]').forEach((group) => {
const control = group.querySelector('input[type="range"]');
const output = group.querySelector('output');
const buttons = group.querySelectorAll('[data-step]');
const min = Number(control.min || 0);
const max = Number(control.max || 100);
const plural = new Intl.PluralRules(document.documentElement.lang || 'en');
const update = () => {
const value = Number(control.value);
const unit = plural.select(value) === 'one' ? group.dataset.unitOne : group.dataset.unitMany;
const text = `${value} ${unit}`;
output.textContent = text;
control.setAttribute('aria-valuetext', text);
buttons.forEach((button) => {
button.disabled = Number(button.dataset.step) < 0 ? value <= min : value >= max;
});
};
buttons.forEach((button) => {
button.addEventListener('click', () => {
Number(button.dataset.step) < 0 ? control.stepDown() : control.stepUp();
update();
control.dispatchEvent(new Event('change', { bubbles: true }));
});
});
control.addEventListener('input', update);
update();
});