@use 'sprucecss/scss/spruce' as *;
.range-group {
display: flex;
flex-direction: column;
gap: spacer('xs');
.form-label {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: spacer('xs');
justify-content: space-between;
&__value {
font-weight: 400;
}
}
&__inner {
align-items: center;
display: flex;
gap: spacer('xs');
.form-range {
flex-grow: 1;
margin-block-start: 0;
}
.form-range-control {
flex-shrink: 0;
}
}
}
<div class="range-group">
<label class="form-label" for="quantity">
<span class="form-label__title">Quantity</span>
<span class="form-label__value" data-suffix="piece">1 piece</span>
</label>
<div class="range-group__inner">
<button class="btn btn--primary btn--sm btn--icon" data-action="decrement" aria-label="Decrement quantity">
<svg aria-hidden='true' fill='none' focusable='false' height='24' stroke-linecap='round' stroke-linejoin='round' stroke-width='3.5' stroke='currentColor' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg' class='btn__icon'>
<line x1='5' y1='12' x2='19' y2='12'></line>
</svg>
</button>
<input class="form-range range-group__control" id="quantity" type="range" min="1" max="99" value="1" step="1" />
<button class="btn btn--primary btn--sm btn--icon" data-action="increment" aria-label="Increment quantity">
<svg aria-hidden='true' fill='none' focusable='false' height='24' stroke-linecap='round' stroke-linejoin='round' stroke-width='3.5' stroke='currentColor' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg' class='btn__icon'>
<line class='vertical-line' x1='12' y1='5' x2='12' y2='19'></line>
<line x1='5' y1='12' x2='19' y2='12'></line>
</svg>
</button>
</div>
</div>
(() => {
const initializeRangeGroup = (rangeGroup) => {
const decrement = rangeGroup.querySelector('[data-action="decrement"]');
const increment = rangeGroup.querySelector('[data-action="increment"]');
const label = rangeGroup.querySelector('.form-label__value');
const suffix = label.getAttribute('data-suffix') || '';
const control = rangeGroup.querySelector('.range-group__control');
const step = parseInt(control.getAttribute('step'), 10) || 1;
const min = parseInt(control.getAttribute('min'), 10) || 0;
const max = parseInt(control.getAttribute('max'), 10) || 100;
const checkDisabledState = (value) => {
decrement.disabled = min === value;
increment.disabled = max === value;
};
const updateValues = (value) => {
control.value = value;
label.textContent = `${value} ${suffix}`;
checkDisabledState(value);
};
control.addEventListener('input', (e) => {
updateValues(parseInt(e.target.value, 10));
});
decrement.addEventListener('click', () => {
if (control.value > min) {
updateValues(parseInt(control.value, 10) - step);
}
});
increment.addEventListener('click', () => {
if (control.value < max) {
updateValues(parseInt(control.value, 10) + step);
}
});
updateValues(parseInt(control.value, 10));
};
const rangeGroups = document.querySelectorAll('.range-group');
if (rangeGroups.length) {
rangeGroups.forEach(initializeRangeGroup);
}
})();