javascripthtmlcssinput-type-range

How do I increase the clickable area of a pseudo element without changing its display size?


UPD2: Check the marked answer for the solution. There's a small bug that with transparency on IOS that I came across which may or may not be there in your case. If you are experiencing it too read the comments on the marked answer. Cheers!

UPD: Check the github repo out if you want to see it in context: https://github.com/LitvinenkoD/activity-generator-based-on-Bored-API

Here is the working version on GitHub Pages https://litvinenkod.github.io/activity-generator-based-on-Bored-API/

I'm working on a min/max range selector using <input type="range">.

I want the thumbs of the selector to be visually small, but have large clickable area for UX purposes.

This is the visual look I need

The thumbs size

And this is the clickable area I want under the hood

The clickable area size

I tried using pseudo elements, but ::-moz-range-thumb and ::-webkit-slider-thumb are already pseudo elements themselves, so there is no option of adding a hidden pseudo element on top of them.

There is also an option of adding an invisible border and then doing background-clip: padding-box;, but that changes the functionality of the input form because it treats the thumbs as if they're actually a lot bigger and it affects the way range in between them looks like.

Is there a solution I could use in my project? I'm ok with doing something 50 lines long as long as I can actually implement the functionality I want.

Adding code here:

HTML

      <div class="slider">
        <input type="range" class="slider__min-selector" min="0" max="10" step="1">
        <input type="range" class="slider__max-selector" min="0" max="10" step="1">
        
        <div class="slider__background-range-box">
          <div class="slider__range_visualizer"></div>
        </div>
      </div>


<!-- Adds selected values visualization -->
      <div class="range-selector__supporting-text | flexbox-container">
        <p class="range-selector__min-text">0</p>
        <p class="range-selector__max-text">10</p>
      </div>
    </div>

CSS

input[type="range"]::-moz-range-thumb{

  // Removing standard look
  -moz-appearance: none;
  border: none;

  // Styling
  background-color: #F0F0F0;
  cursor: pointer;



  // Visual size I need
  height: 1em;
  width: 1em;
  border-radius: 50%;
  

  // Needed for the thumb to be clickable
  pointer-events: auto;




  // I used the line below to demonstrate
  // the clickable area size I need

  border: 10px solid transparent;


  // Adding this element allows me to hide
  // the border, but the browser treats
  // the element as it was actually larger
  background-clip: padding-box;
}

JS

export class RangeSelector{
  constructor(range_selector_min_tip, range_selector_max_tip, range_visualizer, current_range, sliders_min_gap, range_selector_min_text_visualizer, text_max){
    this.range_selector_min_tip = range_selector_min_tip
    this.range_selector_max_tip = range_selector_max_tip
    this.range_visualizer = range_visualizer


    // This array dynamically updates and always stores the value of min and max selectors
    this.current_range = current_range

    // Minimum acceptable distance between selectors
    this.min_selectors_gap = sliders_min_gap

    this.range_selector_min_text_visualizer = range_selector_min_text_visualizer
    this.text_max = text_max

  }

  // Sets values of the actual tips equal to the values we initialized before
  initializeSlider(){
    this.range_selector_min_tip.value = this.current_range[0]
    this.range_selector_max_tip.value = this.current_range[1]
  }


  // Updates the colored gap between two slider tips
  adjustRangeVisualizer(){
    const left = this.current_range[0] * 10
    const right = 100 - this.current_range[1] * 10
  
    this.range_visualizer.style.left = left + "%"
    this.range_visualizer.style.right = right + "%"
  }

  updateRangeText(){
    this.range_selector_min_text_visualizer.innerText = this.range_selector_min_tip.value
    this.text_max.innerText = this.range_selector_max_tip.value
  }
}

Solution

  • I found a mostly-CSS way: use radial-gradient() to create a thumb and modify its position based on the value of the <input>.

    input[type="range"]::-webkit-slider-thumb {
      height: var(--touch-size);
      width: var(--touch-size);
      background: radial-gradient(
        circle at
          calc(
            (100% - var(--thumb-size)) / 10 * var(--value) +
            var(--thumb-size) / 2
          )
          50%,
        #f0f0f0 calc(var(--thumb-size) / 2),
        transparent calc(var(--thumb-size) / 2)
      );
    }
    

    A bit of JS is needed so as to modify the inline custom property:

    inputs.forEach((element, isMax) => {
      element.addEventListener('input', () => {
        element.style.setProperty('--value', element.value);
      });
    });
    

    Try it:

    const visualizer = document.querySelector('.slider__range_visualizer');
    const inputs = document.querySelectorAll('input[type="range"]');
    
    inputs.forEach((element, isMax) => {
      element.addEventListener('input', () => {
        const value = element.value;
        element.style.setProperty('--value', value);
    
        const property = isMax ? 'right' : 'left';
        const percentage = (isMax ? 10 - value : value) * 10;
        visualizer.style[property] = percentage + '%';
      });
    });
    :root {
      --thumb-size: 20px;
      --touch-size: 70px;
    }
    
    input[type="range"]::-webkit-slider-thumb {
      height: var(--touch-size);
      width: var(--touch-size);
      
      background: radial-gradient(
        circle at
          calc(
            (100% - var(--thumb-size)) / 10 * var(--value) +
            var(--thumb-size) / 2
          )
          50%,
        #f0f0f0 calc(var(--thumb-size) / 2),
        transparent calc(var(--thumb-size) / 2)
      );
      
      /* Just to visualize the actual size */
      outline: 1px solid black;
    }
    
    input[type="range"]::-moz-range-thumb {
      height: var(--touch-size);
      width: var(--touch-size);
      
      background: radial-gradient(
        circle at
          calc(
            (100% - var(--thumb-size)) / 10 * var(--value) +
            var(--thumb-size) / 2
          )
          50%,
        #f0f0f0 calc(var(--thumb-size) / 2),
        transparent calc(var(--thumb-size) / 2)
      );
      
      /* Just to visualize the actual size */
      outline: 1px solid black;
    }
    
    
    /* Original styles */
    
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    .slider {
      position: relative;
      margin: 2em 1em;
      width: 500px;
    }
    
    .slider input[type="range"] {
      position: absolute;
      top: 0;
      left: 0;
      appearance: none;
      width: 100%;
      background: none;
      transform: translateY(-50%);
      pointer-events: none;
      z-index: 2;
    }
    
    input[type="range"]::-webkit-slider-thumb {
      appearance: none;
      pointer-events: auto;
      cursor: pointer;
      border-radius: 50%;
    }
    
    input[type="range"]::-moz-range-thumb {
      appearance: none;
      pointer-events: auto;
      cursor: pointer;
      border-radius: 50%;
    }
    
    .slider__background-range-box,
    .slider__range_visualizer {
      height: .25em;
      border-radius: .25em;
    }
    
    .slider__background-range-box {
      width: 100%;
      position: relative;
      transform: translateY(-50%);
      background-color: grey;
    }
    
    .slider__range_visualizer {
      position: absolute;
      left: 0;
      right: 0;
      background-color: pink;
    }
    <div class="slider">
      <input type="range" class="slider__min-selector" min="0" max="10" step="1" value="0" style="--value: 0;">
      <input type="range" class="slider__max-selector" min="0" max="10" step="1" value="10" style="--value: 10;">
      <div class="slider__background-range-box">
        <div class="slider__range_visualizer"></div>
      </div>
    </div>

    A note about licensing: The "Original styles" section were copied from your project, which so far has no license. For the purpose of this answer, I must ask you to release the aforementioned code under CC BY-SA 4.0 or a compatible license. If you refuse to do so, you may flag this answer for moderator attention as copyright violation.