[Feature Requests] Spectrum-Slider options & customizability

I’m writing a slider component right now in which i wrap the sp-slider and rebuild the label option.
Why? Because…

  • By default the sp-slider has unnecessary large height (I can cut that off in my own wrapper)
  • Overall the clickable area is too large in my opinion (on the label and excessive height), which might confuse users
  • I can’t use the sp-label: I do have a fixed set of values (20), which are specific angles,
    for example: 38.65°, 41.99°, 45°, etc.
    I can’t influence the label text, it will always show the specified value. However, I don’t want to have a continuous range of values from 0-90°, instead my range value is from 0-20, specifying the index of the angle in my list.

One problem I’m currently facing is that the onChange event only emits upon mouseup, so my custom label can’t be updated while the user drags the slider:

Unfortunately, the real time updates are critical in this case, so I’ll have to use the native slider for now.

Also, the contrast on the sp-slider track is quite bad paired with the default panel background color. As far as I know spectrum components are not customizable in style.

So these are some feature requests I have for the Spectrum-Slider:

  • implement ticks or specific sets of accepted values
  • alternatively give an option to overwrite the label value (separately from the slider value)
  • remove excessive (clickable) whitespace or at least let us control the height or margin. This applies to other spectrum components, too.
  • add customizability via CSS (colors, fontSizes, etc)
  • add an event that emits upon internal value change, not only after mouseup. Make the input event emit values while dragging (not only when the mouse stops moving) or let the mousemove event emit values also when the mouse leaves the slider area.
1 Like

Hi, for real time label update, you may have to use the event “mousemove” to update the label value, and keep “change” to fire the call to the action.slider

Ah yes, that works (kind of). However it doesn’t emit when you drag outside of the slider, and most users probably won’t drag in such a straight line. (also the value sometimes got undefined)

I’ve tested the input (onInput in React) again and this worked too, maybe I had a misspelling before.
What’s weird is that the input event has some small delay of maybe 100-300ms. Do you have that, too?

Anyways, I’ll cross that point of the feature-request list.


Edit: I now saw what the input event does: It waits for your mouse to rest (no movement for x milliseconds), then emits the value. Still not quite perfect behavior… Also, I’m still wondering which appearance I like better:


I dont use events as “On…”, I use “click”, “mousemove” and “mouseup”. I don’t use “input” at all.

I don’t have any lag time though, 100-300ms seems quite a lot. I’m developping simulatenously on Mac and PC and… so far so good!

OnEvent is just the syntax in React, it’s the same event though. Instead of click+mouseup you can probably also listen to the change event, it emits one value when the mouse is up.

If you try out the input event, you should be able to see the delay. Also, have you tested to move off the element with mousemove?

Edit: Oh, I think I got your point now (using click, mousemove and mouseup alltogether). So do you listen to the click on the slider and mousemove on the window or document? Like this you could read the slider elements value without the mentioned problems.

Yep so basically, I use .addEventListener(“change”) for updating the value and firing Batchplay. I’m using .addEventListener(“mousemove”) to only update the text value.

In a nutshell, I use:

document.getElementById("opacitySlider").addEventListener("change", evt => {
    myOpacity = parseInt(`${evt.target.value}`);
    document.querySelector("#opSliderValue").textContent = myOpacity ? myOpacity + " %" : "0";
    // batchPlay stuff.....

document.getElementById("opacitySlider").addEventListener("mousemove", evt => {
    myOpacity = parseInt(`${evt.target.value}`);
    document.querySelector("#opSliderValue").textContent = myOpacity ? myOpacity + " %" : "0";

When you click and move out of the element, the slider’s head position gets updated but not the value. However, if you click on the slider, hold and move your mouse way outside the UXP panel frame, it’s working again. I havent’ found a solution for that, have you?

PS I’m not using sp-label either in the above example. It’s just sp-details so I can have better flexibility, meeting what you described.

1 Like

Yes I have a solution for that, it all works fine now. Essentially, these are the steps you need to do:

  1. Add a mousedown listener on your element
  2. On mousedown, add a mousemove and mouseup listener to the document
  3. On mousemove, read out the value. I recommend checking it against the previous value to prevent unnecessary many function calls (if you do calculations based on the value). Because not every mousemove equals a change of the slider value.
  4. On mouse up, remove the mousemove listener from the document

That’s a common piece of logic for drag interactions which I’ve already used several times (e.g. for the custom scrollbars), so I don’t know why it took me so long to figure this out :smiley:

Here’s the code: (just a bit of React in there, just ignore that. The this.props.onChange is just a function to delegate the new value to the parent component)

private handleMouseDown = () => {
  document.addEventListener('mousemove', this.handleMouseMove)
  document.addEventListener('mouseup', () => document.removeEventListener('mousemove', this.handleMouseMove))

private handleMouseMove = () => {
  const newValue = parseFloat(this.sliderRef.value)
  if (this.props.onChange && this.props.value !== newValue) {
/* in the view: */
<div onMouseDown={this.handleMouseDown}>
  <sp-slider ref={ref => this.sliderRef = ref} min={min} max={max} value={value} />

Here’s my take (using some CSS & JS) (code below). Notes:

  1. I can’t replicate the issue you’re seeing with onInput; updates are tracking my mouse cursor immediately on both Windows and macOS.
  2. I’m slightly mis-using the “label” slot here by adding another label inside, but that gives me an easy way to target the right side and replace the value. (with show-value="false", there’s no collision)
  3. Text contrast can be changed by specifying the color for the sp-labels (as seen below). You can also modify the font size. Note that there’s no facility to modify the color of the track or thumb.
  4. Selection on label/value; this seems to be a bug, and doesn’t happen on the web version of the component.
  5. A smaller size is coming in the next release. You’d be able to use size="s"
  6. You can use margin-bottom on sp-label to reduce the spacing between label and slider.


sp-slider sp-label {
    color: white;
    font-size: 11px;
    margin-bottom: -6px;
.value {
    position: absolute;
    margin-top: -1px;
    right: 0;
    padding: 0;
    text-align: right;
    min-width: 0;
<sp-slider class="picker" show-value="false" min=0 max=20 value=10>
    <sp-label class="label" slot="label">Degrees
        <sp-label class="value">35.25°</sp-label>
    <option value="0">5.5°</option>
    <option value="1">7.75°</option>
    <option value="2">9.825°</option>
    <option value="3">12.25°</option>
    <option value="4">15.375°</option>
    <option value="5">18.5°</option>
    <option value="6">22.125°</option>
    <option value="7">25°</option>
    <option value="8">27.25°</option>
    <option value="9">29.125°</option>
    <option value="10">35.25°</option>
    <option value="11">45.5°</option>
    <option value="12">52.125°</option>
    <option value="13">55.5°</option>
    <option value="14">57.325°</option>
    <option value="15">63.125°</option>
    <option value="16">67.875°</option>
    <option value="17">73.125°</option>
    <option value="18">75.5°</option>
    <option value="19">83.25°</option>
    <option value="20">90°</option>


document.addEventListener("input", evt => {
    if (evt.target.tagName === "SP-SLIDER" && 
        evt.target.className === "picker") {
        const slider = evt.target;
        const desiredOption = slider.querySelector(`option[value=${slider.value}]`).textContent;
        slider.querySelector("sp-label.value").textContent = desiredOption;

(Could easily be done w/ React instead)

Thanks for taking the time to replicate this, @kerrishotts. Some thoughts on the 6 points:

  1. I’m confused… I just tested it again and onInput works fine for me, too, now! The only explanation I could maybe see for why it didn’t work yesterday is that I didn’t check for actual value change (compared to the last value). The constant flood of values might have slowed down the rendering, although React should take care of something like that :thinking:
    One more thing regarding the events: onInput works fine on the sp-slider directly, however onChange has to be on the WrapperComponent instead to work correctly, why is that?

  2. Clever solution, I wasn’t aware of the fact that any children inside the label would be rendered at the spot on the right.

  3. I probably mixed up the fontSize issue with other Spectrum components, such as the sp-dropdown. (Afaik, the font-size is fixed there)
    I still think that adjusting the track and thumb color would be helpful. Also, I’m wondering why your slider track is lighter than the background, for me its darker:
    My first thought was that you might be on the “darkest” theme, but your panel background is the same as mine.

  4. Since you can already control the width of the whole slider and the font-size, what will size="s" actually impact? The component height or the strokeWidth & size of the track & thumb?

  5. Yes, the margin can be set via CSS. The “unnecessary whitespace” i meant is the one of the sp-slider element itself:
    I got rid of that using a wrapper with a fixed height and display: flex; align-items: center; though

Not sure if your code works for comparing old and new values because you parseFloat the newValue while not parsing the old value, what should mean both values are always unequal?
As said i’m not sure, because i’m not familiar with neither react nor with “this”… i`m still learning.
However, i’m stuck at a similar point where input / mousemove fire too often. I tried several methods for getting a better performance without unneccessary calls, but had no luck so far.

I’m quite sure the code was working correctly. The new value is the one read inside the event handler, directly from the event target - therefore it’s a string and has to be parsed. The value I compared it against was the current value, taken from the components state (which was already parsed when it got written to the state).