Skip to content

Slider Fill Origin

Use fill-origin for an adjustment that fills from a configurable baseline instead of always filling from the minimum. The paired number fields provide precise input while the customized slider remains useful for quick visual adjustment.

Baseline adjustment

Set a value relative to a configurable neutral point.

+25%

The filled interval runs between the baseline and the current value.

Code

The following example assumes that you have imported the component and set up the theme.

Baseline Adjustment
<!-- eslint-disable html/use-baseline -->
<form class="adjustment-panel">
<header class="adjustment-header">
6 collapsed lines
<div>
<h2>Baseline adjustment</h2>
<p>Set a value relative to a configurable neutral point.</p>
</div>
<output class="adjustment-output" for="adjustment-value adjustment-origin">+25%</output>
</header>
<div class="slider-field">
<label for="baseline-adjustment">Adjustment</label>
<sv-slider
id="baseline-adjustment"
class="adjustment-slider"
aria-describedby="adjustment-help"
fill-origin="0"
max="100"
min="-100"
name="adjustment"
step="5"
value="25"
>
<div class="track-overlay" aria-hidden="true" slot="track-content">
<span class="origin-line"><span class="origin-label">Baseline 0</span></span>
</div>
<span class="custom-thumb" aria-hidden="true" slot="thumb-1"><span></span></span>
<span class="value-tooltip" aria-hidden="true" slot="tooltip">
<strong data-value="0"></strong>
%
</span>
<datalist>
<option label="Minimum, -100 percent" value="-100"></option>
<option label="Negative midpoint, -50 percent" value="-50"></option>
<option label="Neutral, 0 percent" value="0"></option>
<option label="Positive midpoint, +50 percent" value="50"></option>
<option label="Maximum, +100 percent" value="100"></option>
</datalist>
</sv-slider>
<div class="scale-labels" aria-hidden="true">
<span>−100</span>
<span>+100</span>
</div>
<p id="adjustment-help">The filled interval runs between the baseline and the current value.</p>
</div>
<div class="adjustment-controls">
17 collapsed lines
<label for="adjustment-value">
<span>Value</span>
<span class="number-field">
<input id="adjustment-value" type="number" max="100" min="-100" step="5" value="25" />
<span>%</span>
</span>
</label>
<label for="adjustment-origin">
<span>Baseline</span>
<span class="number-field">
<input id="adjustment-origin" type="number" max="100" min="-100" step="5" value="0" />
<span>%</span>
</span>
</label>
<button type="button" class="reset-button">Reset</button>
</div>
</form>
<script type="module">
72 collapsed lines
const panel = document.querySelector(".adjustment-panel");
/** @type {Slider} */
const slider = panel.querySelector(".adjustment-slider");
/** @type {HTMLInputElement} */
const valueInput = panel.querySelector("#adjustment-value");
/** @type {HTMLInputElement} */
const originInput = panel.querySelector("#adjustment-origin");
const output = panel.querySelector(".adjustment-output");
const originLabel = panel.querySelector(".origin-label");
const resetButton = panel.querySelector(".reset-button");
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
slider.valueLabelHandler = value => `${formatSigned(value)} percent`;
updateDisplay();
slider.addEventListener("valuechange", event => updateDisplay(event.detail.value));
valueInput.addEventListener("change", () => {
const value = valueInput.valueAsNumber;
if (!valueInput.validity.valid || !Number.isFinite(value)) {
valueInput.value = String(getSliderValue());
return;
}
slider.setValue({ value, animate: !reducedMotion.matches, duration: 250 });
updateDisplay(value);
});
originInput.addEventListener("change", () => {
const origin = originInput.valueAsNumber;
if (!originInput.validity.valid || !Number.isFinite(origin)) {
originInput.value = String(slider.fillOrigin ?? slider.min);
return;
}
slider.fillOrigin = origin;
updateDisplay();
});
resetButton.addEventListener("click", () => {
originInput.value = "0";
slider.fillOrigin = 0;
slider.setValue({ value: 25, animate: !reducedMotion.matches, duration: 350 });
updateDisplay(25);
});
/** @param {number} [value] */
function updateDisplay(value = getSliderValue()) {
const origin = slider.fillOrigin ?? slider.min;
const clampedOrigin = Math.min(Math.max(slider.min, origin), slider.max);
const originPercentage = ((clampedOrigin - slider.min) / (slider.max - slider.min)) * 100;
valueInput.value = String(value);
output.textContent = `${formatSigned(value)}%`;
originLabel.textContent = `Baseline ${formatSigned(origin)}`;
slider.style.setProperty("--origin-position", `${originPercentage}%`);
}
function getSliderValue() {
const value = slider.value;
if (Array.isArray(value)) return value[0] ?? slider.min;
return value;
}
/** @param {number} value */
function formatSigned(value) {
return value > 0 ? `+${value}` : String(value);
}
</script>
<style>
.adjustment-panel {
9 collapsed lines
display: grid;
gap: 1.5rem;
max-inline-size: 46rem;
padding: clamp(1rem, 4vw, 2rem);
margin: auto;
color: var(--sv-gray-1);
background: var(--sv-gray-7);
border: var(--sv-bdr-sz-sm, 1px) solid var(--sv-gray-5);
border-radius: var(--sv-bdr-rad-lg, 16px);
}
.adjustment-header {
20 collapsed lines
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: start;
justify-content: space-between;
h2,
p {
margin: 0;
}
h2 {
font-size: 1.15rem;
}
p {
margin-block-start: 0.35rem;
font-size: 0.85rem;
color: var(--sv-gray-3);
}
}
.adjustment-output {
8 collapsed lines
min-inline-size: 4.5rem;
padding: 0.55rem 0.75rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--sv-accent-content);
text-align: center;
background: var(--sv-accent);
border-radius: var(--sv-bdr-rad-md, 8px);
}
.slider-field {
13 collapsed lines
display: grid;
gap: 0.7rem;
> label {
font-size: 0.8rem;
font-weight: 650;
}
> p {
margin: 0;
font-size: 0.75rem;
color: var(--sv-gray-3);
}
}
.custom-thumb {
16 collapsed lines
display: grid;
place-items: center;
inline-size: var(--sv-thumb-size);
block-size: var(--sv-thumb-size);
background: var(--sv-gray-7);
border: 2px solid var(--sv-accent);
border-radius: 50%;
box-shadow: 0 2px 6px rgb(0 0 0 / 18%);
transition: scale 150ms ease;
> span {
inline-size: 0.4rem;
block-size: 0.4rem;
background: var(--sv-accent);
border-radius: 50%;
}
}
.adjustment-slider {
--sv-thickness: 1.6rem;
--sv-thumb-size: 2rem;
--sv-bdr-rad-md: 999px;
--sv-tooltip-offset: 2rem;
--origin-translate: -50%;
inline-size: 100%;
&:dir(rtl) {
--origin-translate: 50%;
}
&::part(track) {
background: var(--sv-gray-6);
border-color: var(--sv-gray-5);
}
&::part(fill) {
background: var(--sv-accent);
border-radius: 0;
}
&::part(marker) {
inline-size: 0.35rem;
block-size: 0.35rem;
background: var(--sv-gray-2);
border: 2px solid var(--sv-gray-7);
border-radius: 50%;
}
&::part(tooltip) {
padding: 0;
color: var(--sv-accent-content);
background: var(--sv-accent);
border: 0;
}
&::part(tooltip-arrow) {
fill: var(--sv-accent);
stroke: transparent;
}
&:state(--active) .custom-thumb {
scale: 0.9;
}
}
.track-overlay {
4 collapsed lines
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.origin-line {
18 collapsed lines
position: absolute;
inset-block: 0;
inset-inline: var(--origin-position) auto;
inline-size: 2px;
background: var(--sv-gray-2);
translate: var(--origin-translate);
.origin-label {
position: absolute;
inset-block: 100% auto;
inset-inline: 50% auto;
min-inline-size: max-content;
margin-block-start: 0.8rem;
font-size: 0.65rem;
font-variant-numeric: tabular-nums;
color: var(--sv-gray-3);
translate: var(--origin-translate);
}
}
.value-tooltip {
8 collapsed lines
display: inline-flex;
align-items: baseline;
justify-content: center;
min-inline-size: 3rem;
padding: 0.35rem 0.5rem;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
border-radius: var(--sv-bdr-rad-md, 8px);
}
.scale-labels {
6 collapsed lines
display: flex;
justify-content: space-between;
padding-inline: 0.15rem;
font-size: 0.65rem;
font-variant-numeric: tabular-nums;
color: var(--sv-gray-3);
}
.adjustment-controls {
11 collapsed lines
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
gap: 0.75rem;
align-items: end;
> label {
display: grid;
gap: 0.4rem;
font-size: 0.75rem;
color: var(--sv-gray-3);
}
}
.number-field {
22 collapsed lines
display: flex;
align-items: center;
min-inline-size: 0;
color: var(--sv-gray-3);
background: var(--sv-gray-6);
border: var(--sv-bdr-sz-sm, 1px) solid var(--sv-gray-5);
border-radius: var(--sv-bdr-rad-md, 8px);
input {
inline-size: 100%;
min-inline-size: 0;
block-size: 2.5rem;
padding-inline: 0.7rem;
font: inherit;
color: var(--sv-gray-1);
background: transparent;
border: 0;
}
> span {
padding-inline-end: 0.7rem;
}
}
.reset-button {
8 collapsed lines
min-block-size: calc(2.5rem + 2px);
padding-inline: 1rem;
font: inherit;
color: var(--sv-accent-content);
cursor: pointer;
background: var(--sv-accent);
border: 0;
border-radius: var(--sv-bdr-rad-md, 8px);
}
20 collapsed lines
.adjustment-panel :is(input, button):focus-visible {
outline: 2px solid var(--sv-accent);
outline-offset: 2px;
}
@media (width < 32rem) {
.adjustment-controls {
grid-template-columns: 1fr 1fr;
.reset-button {
grid-column: 1 / -1;
}
}
}
@media (prefers-reduced-motion: reduce) {
.custom-thumb {
transition-duration: 0s;
}
}
</style>