Recreating Laravel Cloud’s range input with native HTML


For the past few days, I’ve been working on improving the billing experience in Phare, with the addition of prepaid credits. While tweaking the billing UI, I realized the current input for configuring additional quota wasn’t great, it didn’t clearly show what was already included in the paid plan versus what the user could add.

The UX of entering large number in a text input could certainly be improved. It wasn't horrible, but I felt it could be better.

Recreating Laravel Cloud’s range input with native HTML

So I went hunting for inspiration on Dribbble, and other listing websites that post screenshots of SaaS services interfaces. After some research, I stumbled upon the Laravel Cloud pricing calculator. Their range input design was spot-on: clear separation between included and additional values, visually appealing, and user-friendly.

Recreating Laravel Cloud’s range input with native HTML

Naturally, I did what any self-respecting developer would do, open the browser inspector to steal look at the code. Turns out, they recreated a full range input with a few HTML elements and glued everything with JavaScript using Alpine.js. Here's the structure:

<div class="group/range relative h-8 self-stretch">
  <!– Full track -->
  <div />

  <!-- Static track -->
  <div />

  <!-- Progress bar -->
  <div />

  <!-- Handle -->
  <div>
    <div>
      <span></span>
    </div>
  </div>

  <!-- Tick -->
  <div />

  <!-- HTML range input -->
  <input />
</div>

Because I'm the laziest developer, rebuilding all this for the six people (love you guys 🫶) that pay for Phare felt like overkill. Could I recreate a similar input with less work? There's probably a way to get a similar result with some CSS on top of the native range input, and maybe a few lines of JavaScript.

Building the range input

To match the Laravel Cloud design, we need the following components:

Range track : the rail where the handle moves.
Handle : the draggable thumb.
Progress bar : the filled area left of the handle.
Static part : a fixed section showing the value already included in the plan.
Tick : a visual marker where the included value ends and extra begins.

Recreating Laravel Cloud’s range input with native HTML

The static part and tick are mostly cosmetic and can easily be visually faked outside the range input itself. Everything else is already included in the native HTML range input.

So why did the Laravel team go full custom?

Limitations of the native HTML range input

To look great, the handle needs to land exactly at the tick’s position when at the minimum value. It should also cover the tick to be visually appealing:

Recreating Laravel Cloud’s range input with native HTML

Unfortunately, this isn't possible as the native range input’s handle is confined to the boundaries of the track. So, what if we make the range input track overlap under the static part to allow the handle to sit on the tick? (such a weird sentence).

Well, the native range inputs don’t let us set a different z-index for the handle and for the track. If we push the track behind the static part, the handle goes with it. If you bring it forward, the whole thing looks messy.

Recreating Laravel Cloud’s range input with native HTML

The solution

Enter the CSS inner shadow : using an inner shadow allows us to fake a few extra pixels of the static part inside the track. This lets the handle glide over it without getting hidden.

By carefully layering the tick and the static track visually outside the actual input, and using this inner shadow to fake part of the static part, we can get something that works well.

Recreating Laravel Cloud’s range input with native HTML

Styling the handle

Using the border property on the handle with -moz-range-thumb work great in Firefox, but Chrome does not seem to support it. Again, inner shadows are here to save the day, and bring us cross browser consistency.

Styling the progress bar

To make the progress bar pattern, Laravel's team used a clever trick based on repeating-linear-gradient to create infinitely repeating stripes.

background-image: repeating-linear-gradient(135deg, black 0px, black 1px, #99a1af 1px, #99a1af 4px);

But applying that to our native range input will cover the entire track. I only wanted it on the left side of the handle to represent progress.

Recreating Laravel Cloud’s range input with native HTML

For fixing this, there isn't any other solution, we will need a few lines of JavaScript:

document.getElementById("range").addEventListener('input', function (event) {
    let input = event.target
    let value = parseInt(input.value)
    let min = parseInt(input.getAttribute('min'))
    let max = parseInt(input.getAttribute('max'))

    let percentage = (value - min) / (max - min) * 100

    input.style.backgroundSize = `${percentage}% 100%, 100% 100%`
})

Final result

The end result is not quite as flexible as Laravel Cloud’s full custom implementation. Since the track should fake the design of the static part and tick, it does not allow more complex design, but it fits perfectly for my use case:

Final input version

Conclusion

The final native HTML approach is quite simple, with minimal tricks, and still good-looking. I think it shows that it's possible to go quite far with native elements without having to resort to recreating everything with JavaScript.

You can see a fully working example and the code to recreate the input on CodePen:

And if you like my attention to details, you should try Phare, it's a great tool for uptime monitoring, incident management, and status pages.

0
Subscribe to my newsletter

Read articles from Nicolas Beauvais directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nicolas Beauvais
Nicolas Beauvais

Founder & CTO of Phare.io