Skip to content
GitHub Codepen bluesky

Input Range Slider Value without JavaScript

Reconstructing a Range Slider's Value Using Only CSS

When I started this experiment, I wasn't actually trying to build a custom range slider.

I was exploring some of the newer CSS features—Anchor Positioning, Container Query Units and progress()—when a question occurred to me:

Could CSS determine the value of a range slider without reading the slider's value?

Traditionally, displaying a value above a range slider requires JavaScript:

slider.addEventListener('input', () => {
    output.textContent = slider.value;
});
slider.addEventListener('input', () => {
    output.textContent = slider.value;
});

CSS has never had access to a range input's current value, so this seemed impossible.

Or at least it used to.

The Idea

Modern CSS gives us access to something almost as useful as a slider's value:

its geometry.

Using Anchor Positioning, the slider thumb can be treated as an anchor. That means other elements can be positioned relative to the thumb as it moves.

For example:

input[type=range]::-webkit-slider-thumb {
    anchor-name: --thumb;
}
input[type=range]::-webkit-slider-thumb {
    anchor-name: --thumb;
}

A value bubble can then be positioned above the thumb using:

.value {
    position-anchor: --thumb;
    left: anchor(center);
    top: anchor(top);
}
.value {
    position-anchor: --thumb;
    left: anchor(center);
    top: anchor(top);
}

This alone doesn't tell us the slider's value, but it does tell us where the thumb is.

And if we know where the thumb is, perhaps we can calculate the value ourselves.

Converting Position into Width

The breakthrough came from creating an invisible measuring element.

This element stretches between the start of the slider track and the thumb position:

Slider Start                    Thumb
|--------------------------------|
<--------- measure width ------->
Slider Start                    Thumb
|--------------------------------|
<--------- measure width ------->

Using Anchor Positioning:

.measure {
    left: anchor(--slider left);
    right: anchor(--thumb left);
}
.measure {
    left: anchor(--slider left);
    right: anchor(--thumb left);
}

As the thumb moves, the width of .measure changes.

At the minimum value:

measure width = 0
measure width = 0

At the maximum value:

measure width = maximum thumb travel
measure width = maximum thumb travel

In other words, the width of this element becomes a physical representation of the slider's state.

Measuring the Width

Normally CSS cannot read an element's dimensions.

However, Container Query Units give us a loophole.

By turning .measure into a size container:

.measure {
    container-type: inline-size;
}
.measure {
    container-type: inline-size;
}

we gain access to:

100cqi
100cqi

which equals the current width of the element.

Suddenly the slider's position has become a number.

Normalising the Measurement

The measured width still isn't very useful:

0px → 284px
0px → 284px

depending on the thumb's position.

What we really want is a value between 0 and 1.

This is where progress() comes in:

--progress: progress(
    100cqi,
    0px,
    calc(var(--slider-width) - var(--thumb-size))
);
--progress: progress(
    100cqi,
    0px,
    calc(var(--slider-width) - var(--thumb-size))
);

Conceptually:

progress(current, minimum, maximum)
progress(current, minimum, maximum)

returns:

(current - minimum)
÷
(maximum - minimum)
(current - minimum)
÷
(maximum - minimum)

Result:

0px     → 0
142px   → 0.5
284px   → 1
0px     → 0
142px   → 0.5
284px   → 1

The slider position has now been converted into a normalised signal.

Reconstructing the Slider Value

Once we have a normalised value, mapping it back to the slider's range becomes straightforward.

--value: calc(
    var(--slider-min) +
    var(--progress) *
    (var(--slider-max) - var(--slider-min))
);
--value: calc(
    var(--slider-min) +
    var(--progress) *
    (var(--slider-max) - var(--slider-min))
);

This is simply linear interpolation.

For a slider ranging from 1 to 50:

progress = 0   → 1
progress = 0.5 → 25.5
progress = 1   → 50
progress = 0   → 1
progress = 0.5 → 25.5
progress = 1   → 50

At this point CSS has effectively reconstructed the slider's value without ever reading the slider itself.

Displaying the Value

CSS still cannot directly convert a calculated number into text.

However, counters provide a useful workaround.

counter-reset: value var(--value);
content: counter(value);
counter-reset: value var(--value);
content: counter(value);

This allows the calculated value to be displayed in a pseudo-element positioned above the thumb.

No JavaScript required.

What I Found Interesting

The most surprising thing about this experiment is that CSS never reads the slider value.

Instead, it derives the value from geometry.

The pipeline looks like this:

Thumb Position
        ↓
Anchor Positioning
        ↓
Measuring Element Width
        ↓
Container Query Units (cqi)
        ↓
progress()
        ↓
Slider Value
        ↓
Counter Output
Thumb Position
        ↓
Anchor Positioning
        ↓
Measuring Element Width
        ↓
Container Query Units (cqi)
        ↓
progress()
        ↓
Slider Value
        ↓
Counter Output

In other words:

CSS is not observing state.

CSS is inferring state from layout.

And that feels like a very different way of thinking about what CSS can do.

Browser Support

This technique relies on several modern CSS features:

  • Anchor Positioning
  • Container Query Units (cqi)
  • progress()
  • round()
  • Typed attr()

At the time of writing, support is limited to modern browsers implementing these features.

This is definitely an experiment rather than a production technique.

But it demonstrates something I didn't expect:

With enough geometry, CSS can reconstruct information that it has never been given directly.

Failed Approaches

As with most experiments, the final solution wasn't the first one.

Some of the ideas explored along the way included:

  • Trying to derive the value directly from the thumb anchor position.
  • Investigating whether progress() could operate on anchor coordinates.
  • Looking at Style Queries and if() as potential shortcuts.
  • Discovering that CSS counters can only output integers.
  • Exploring whether a custom property could somehow expose an anchor's position.
  • Realising that measuring geometry was easier than accessing state.

In hindsight, the key insight was surprisingly simple:

Don't try to read the value.

Measure something that changes when the value changes.

Once the thumb's movement could be represented as a measurable width, the rest of the solution naturally followed.