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.
Code
The following example assumes that you have imported the component and set up the theme.
<!-- 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>