17 CSS Range Sliders10 / 17

CSS + JSMIT licensed

Bubble Pop Tooltip Slider

A loan calculator slider where pressing the thumb pops up a teardrop speech bubble with a spring scale animation, live loan amount and monthly payment calculations update in real time, and the bubble tracks the thumb position precisely.

Published

Live Demo
Try it

The code

<div class="rs-10">
  <div class="rs-10__card">
    <div class="rs-10__header">
      <div class="rs-10__title">Loan Calculator</div>
      <div class="rs-10__sub">Drag to adjust your loan amount</div>
    </div>
    <div class="rs-10__amount-row">
      <span class="rs-10__currency">$</span>
      <span class="rs-10__amount" id="rs-10-amount">15,000</span>
      <span class="rs-10__period">loan</span>
    </div>
    <div class="rs-10__track-wrap" id="rs-10-wrap">
      <div class="rs-10__bubble" id="rs-10-bubble">
        <div class="rs-10__bubble-body" id="rs-10-tip">$15,000</div>
        <div class="rs-10__bubble-arrow"></div>
      </div>
      <div class="rs-10__track-bg"></div>
      <div class="rs-10__track-fill" id="rs-10-fill"></div>
      <input class="rs-10__input" type="range" min="1000" max="50000" step="500" value="15000" id="rs-10-inp">
    </div>
    <div class="rs-10__marks">
      <span class="rs-10__mark">$1K</span>
      <span class="rs-10__mark">$10K</span>
      <span class="rs-10__mark">$25K</span>
      <span class="rs-10__mark">$50K</span>
    </div>
    <div class="rs-10__breakdown">
      <div class="rs-10__item">
        <div class="rs-10__item-label">Monthly</div>
        <div class="rs-10__item-val" id="rs-10-mo">$298</div>
      </div>
      <div class="rs-10__item">
        <div class="rs-10__item-label">Rate APR</div>
        <div class="rs-10__item-val">6.9%</div>
      </div>
      <div class="rs-10__item">
        <div class="rs-10__item-label">Term</div>
        <div class="rs-10__item-val">60 mo</div>
      </div>
    </div>
  </div>
</div>
.rs-10,
.rs-10 *,
.rs-10 *::before,
.rs-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.rs-10 ::selection {
  background: #7c3aed;
  color: #fff;
}

.rs-10 {
  --bg: #1e1b4b;
  --card: #2e2a60;
  --violet: #7c3aed;
  --violet-light: #a78bfa;
  --violet-glow: rgba(124,58,237,0.5);
  --text: #ede9fe;
  --muted: #8b80c4;
  --border: #3d3875;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 24px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.rs-10__card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 24px;
  padding: 36px;
  max-width: 440px;
  width: 100%;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}

.rs-10__header {
  margin-bottom: 12px;
}

.rs-10__title {
  font-size: 18px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 4px;
}

.rs-10__sub {
  font-size: 13px;
  color: var(--muted);
}

.rs-10__amount-row {
  display: flex;
  align-items: baseline;
  gap: 4px;
  margin: 20px 0 44px;
}

.rs-10__currency {
  font-size: 24px;
  color: var(--violet-light);
  font-weight: 700;
}

.rs-10__amount {
  font-size: 56px;
  font-weight: 900;
  color: var(--text);
  letter-spacing: -3px;
  font-variant-numeric: tabular-nums;
}

.rs-10__period {
  font-size: 14px;
  color: var(--muted);
  align-self: flex-end;
  margin-bottom: 8px;
}

.rs-10__track-wrap {
  position: relative;
  padding-top: 54px;
  margin-bottom: 16px;
}

.rs-10__bubble {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  transition: left 0.05s;
}

.rs-10__bubble-body {
  background: var(--violet);
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  padding: 6px 14px;
  border-radius: 20px;
  white-space: nowrap;
  box-shadow: 0 4px 20px var(--violet-glow);
  transform: scale(0);
  transform-origin: bottom center;
  transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1);
}

.rs-10__bubble-arrow {
  width: 0;
  height: 0;
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-top: 8px solid var(--violet);
  margin-top: -1px;
  opacity: 0;
  transition: opacity 0.2s;
}

.rs-10__track-wrap.active .rs-10__bubble-body {
  transform: scale(1);
}

.rs-10__track-wrap.active .rs-10__bubble-arrow {
  opacity: 1;
}

.rs-10__track-bg {
  height: 8px;
  background: rgba(255,255,255,0.08);
  border-radius: 99px;
}

.rs-10__track-fill {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 8px;
  background: linear-gradient(90deg, var(--violet), var(--violet-light));
  border-radius: 99px;
  box-shadow: 0 0 16px var(--violet-glow);
  transition: width 0.05s;
}

.rs-10__input {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 36px;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
  outline: none;
  z-index: 2;
  margin-bottom: -14px;
}

.rs-10__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--violet);
  box-shadow: 0 0 12px var(--violet-glow);
  cursor: grab;
  transition: transform 0.15s;
}

.rs-10__input:active::-webkit-slider-thumb {
  transform: scale(1.15);
  cursor: grabbing;
}

.rs-10__input::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 3px solid var(--violet);
  background: #fff;
  cursor: grab;
}

.rs-10__marks {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
}

.rs-10__mark {
  font-size: 11px;
  color: var(--muted);
}

.rs-10__breakdown {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 0;
}

.rs-10__item {
  flex: 1;
  text-align: center;
}

.rs-10__item + .rs-10__item {
  border-left: 1px solid var(--border);
}

.rs-10__item-label {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}

.rs-10__item-val {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}

@media (prefers-reduced-motion: reduce) {
  .rs-10__bubble-body,
    .rs-10__bubble-arrow,
    .rs-10__track-fill {
    transition: none;
  }

  .rs-10__bubble-body {
    transform: scale(1);
  }

  .rs-10__bubble-arrow {
    opacity: 1;
  }
}
(function(){
  const inp = document.getElementById('rs-10-inp');
  const wrap = document.getElementById('rs-10-wrap');
  const bubble = document.getElementById('rs-10-bubble');
  const tip = document.getElementById('rs-10-tip');
  const fill = document.getElementById('rs-10-fill');
  const amount = document.getElementById('rs-10-amount');
  const mo = document.getElementById('rs-10-mo');
  function fmt(n){ return n>=1000? '$'+Math.round(n/1000)+'K' : '$'+n; }
  function update(){
    const v = +inp.value;
    const pct = (v - +inp.min)/(+inp.max - +inp.min)*100;
    fill.style.width = pct+'%';
    bubble.style.left = pct+'%';
    tip.textContent = fmt(v);
    amount.textContent = v.toLocaleString();
    // simple monthly calc: P*r*(1+r)^n / ((1+r)^n-1)
    const r = 0.069/12, n = 60;
    const m = Math.round(v*r*Math.pow(1+r,n)/(Math.pow(1+r,n)-1));
    mo.textContent = '$'+m.toLocaleString();
  }
  inp.addEventListener('mousedown', ()=> wrap.classList.add('active'));
  inp.addEventListener('touchstart', ()=> wrap.classList.add('active'), {passive:true});
  document.addEventListener('mouseup', ()=> wrap.classList.remove('active'));
  document.addEventListener('touchend', ()=> wrap.classList.remove('active'));
  inp.addEventListener('input', update);
  update();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Range Slider from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Bubble Pop Tooltip Slider
Source: https://codefronts.com/components/css-range-sliders/bubble-pop-tooltip-slider/

A loan calculator slider where pressing the thumb pops up a teardrop speech bubble with a spring scale animation, live loan amount and monthly payment calculations update in real time, and the bubble tracks the thumb position precisely.
## HTML
```html
<div class="rs-10">
  <div class="rs-10__card">
    <div class="rs-10__header">
      <div class="rs-10__title">Loan Calculator</div>
      <div class="rs-10__sub">Drag to adjust your loan amount</div>
    </div>
    <div class="rs-10__amount-row">
      <span class="rs-10__currency">$</span>
      <span class="rs-10__amount" id="rs-10-amount">15,000</span>
      <span class="rs-10__period">loan</span>
    </div>
    <div class="rs-10__track-wrap" id="rs-10-wrap">
      <div class="rs-10__bubble" id="rs-10-bubble">
        <div class="rs-10__bubble-body" id="rs-10-tip">$15,000</div>
        <div class="rs-10__bubble-arrow"></div>
      </div>
      <div class="rs-10__track-bg"></div>
      <div class="rs-10__track-fill" id="rs-10-fill"></div>
      <input class="rs-10__input" type="range" min="1000" max="50000" step="500" value="15000" id="rs-10-inp">
    </div>
    <div class="rs-10__marks">
      <span class="rs-10__mark">$1K</span>
      <span class="rs-10__mark">$10K</span>
      <span class="rs-10__mark">$25K</span>
      <span class="rs-10__mark">$50K</span>
    </div>
    <div class="rs-10__breakdown">
      <div class="rs-10__item">
        <div class="rs-10__item-label">Monthly</div>
        <div class="rs-10__item-val" id="rs-10-mo">$298</div>
      </div>
      <div class="rs-10__item">
        <div class="rs-10__item-label">Rate APR</div>
        <div class="rs-10__item-val">6.9%</div>
      </div>
      <div class="rs-10__item">
        <div class="rs-10__item-label">Term</div>
        <div class="rs-10__item-val">60 mo</div>
      </div>
    </div>
  </div>
</div>
```
## CSS
```css
.rs-10,
.rs-10 *,
.rs-10 *::before,
.rs-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.rs-10 ::selection {
  background: #7c3aed;
  color: #fff;
}

.rs-10 {
  --bg: #1e1b4b;
  --card: #2e2a60;
  --violet: #7c3aed;
  --violet-light: #a78bfa;
  --violet-glow: rgba(124,58,237,0.5);
  --text: #ede9fe;
  --muted: #8b80c4;
  --border: #3d3875;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 24px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.rs-10__card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 24px;
  padding: 36px;
  max-width: 440px;
  width: 100%;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}

.rs-10__header {
  margin-bottom: 12px;
}

.rs-10__title {
  font-size: 18px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 4px;
}

.rs-10__sub {
  font-size: 13px;
  color: var(--muted);
}

.rs-10__amount-row {
  display: flex;
  align-items: baseline;
  gap: 4px;
  margin: 20px 0 44px;
}

.rs-10__currency {
  font-size: 24px;
  color: var(--violet-light);
  font-weight: 700;
}

.rs-10__amount {
  font-size: 56px;
  font-weight: 900;
  color: var(--text);
  letter-spacing: -3px;
  font-variant-numeric: tabular-nums;
}

.rs-10__period {
  font-size: 14px;
  color: var(--muted);
  align-self: flex-end;
  margin-bottom: 8px;
}

.rs-10__track-wrap {
  position: relative;
  padding-top: 54px;
  margin-bottom: 16px;
}

.rs-10__bubble {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  transition: left 0.05s;
}

.rs-10__bubble-body {
  background: var(--violet);
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  padding: 6px 14px;
  border-radius: 20px;
  white-space: nowrap;
  box-shadow: 0 4px 20px var(--violet-glow);
  transform: scale(0);
  transform-origin: bottom center;
  transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1);
}

.rs-10__bubble-arrow {
  width: 0;
  height: 0;
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-top: 8px solid var(--violet);
  margin-top: -1px;
  opacity: 0;
  transition: opacity 0.2s;
}

.rs-10__track-wrap.active .rs-10__bubble-body {
  transform: scale(1);
}

.rs-10__track-wrap.active .rs-10__bubble-arrow {
  opacity: 1;
}

.rs-10__track-bg {
  height: 8px;
  background: rgba(255,255,255,0.08);
  border-radius: 99px;
}

.rs-10__track-fill {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 8px;
  background: linear-gradient(90deg, var(--violet), var(--violet-light));
  border-radius: 99px;
  box-shadow: 0 0 16px var(--violet-glow);
  transition: width 0.05s;
}

.rs-10__input {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 36px;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
  outline: none;
  z-index: 2;
  margin-bottom: -14px;
}

.rs-10__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--violet);
  box-shadow: 0 0 12px var(--violet-glow);
  cursor: grab;
  transition: transform 0.15s;
}

.rs-10__input:active::-webkit-slider-thumb {
  transform: scale(1.15);
  cursor: grabbing;
}

.rs-10__input::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 3px solid var(--violet);
  background: #fff;
  cursor: grab;
}

.rs-10__marks {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
}

.rs-10__mark {
  font-size: 11px;
  color: var(--muted);
}

.rs-10__breakdown {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 0;
}

.rs-10__item {
  flex: 1;
  text-align: center;
}

.rs-10__item + .rs-10__item {
  border-left: 1px solid var(--border);
}

.rs-10__item-label {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}

.rs-10__item-val {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}

@media (prefers-reduced-motion: reduce) {
  .rs-10__bubble-body,
    .rs-10__bubble-arrow,
    .rs-10__track-fill {
    transition: none;
  }

  .rs-10__bubble-body {
    transform: scale(1);
  }

  .rs-10__bubble-arrow {
    opacity: 1;
  }
}
```

## JavaScript
```js
(function(){
  const inp = document.getElementById('rs-10-inp');
  const wrap = document.getElementById('rs-10-wrap');
  const bubble = document.getElementById('rs-10-bubble');
  const tip = document.getElementById('rs-10-tip');
  const fill = document.getElementById('rs-10-fill');
  const amount = document.getElementById('rs-10-amount');
  const mo = document.getElementById('rs-10-mo');
  function fmt(n){ return n>=1000? '$'+Math.round(n/1000)+'K' : '$'+n; }
  function update(){
    const v = +inp.value;
    const pct = (v - +inp.min)/(+inp.max - +inp.min)*100;
    fill.style.width = pct+'%';
    bubble.style.left = pct+'%';
    tip.textContent = fmt(v);
    amount.textContent = v.toLocaleString();
    // simple monthly calc: P*r*(1+r)^n / ((1+r)^n-1)
    const r = 0.069/12, n = 60;
    const m = Math.round(v*r*Math.pow(1+r,n)/(Math.pow(1+r,n)-1));
    mo.textContent = '$'+m.toLocaleString();
  }
  inp.addEventListener('mousedown', ()=> wrap.classList.add('active'));
  inp.addEventListener('touchstart', ()=> wrap.classList.add('active'), {passive:true});
  document.addEventListener('mouseup', ()=> wrap.classList.remove('active'));
  document.addEventListener('touchend', ()=> wrap.classList.remove('active'));
  inp.addEventListener('input', update);
  update();
})();
```

How this works

The bubble is a flexbox column containing a rounded pill label and an arrow triangle div. On mousedown/touchstart, a JS handler adds .active to the wrapper, which triggers the CSS transition on the bubble body: transform: scale(0) → scale(1) with a cubic-bezier(0.34,1.56,0.64,1) spring, making it appear to pop out of the thumb. The bubble is positioned absolutely with left: pct% and transform: translateX(-50%) so it always centers over the current thumb position.

Monthly payment uses the standard amortization formula: P × r × (1+r)^n / ((1+r)^n − 1) where r = APR/12 and n = 60 months, computed directly in JS with no external library. The displayed loan amount uses toLocaleString() for locale-appropriate number formatting. The fmt() helper converts raw values to display strings like $15K for the bubble label.

Make it yours

  • Change loan parameters by updating min, max, and step on the input — also update the tick mark labels and the fmt() threshold from 1000 to match the new scale.
  • Modify the APR by changing const r = 0.069/12 to another rate, or make it interactive by adding a second input and reading its value in the monthly payment formula.
  • Change the spring feel by editing the cubic-bezier(0.34,1.56,0.64,1) on .rs-10__bubble-body — increase the third parameter past 1 for a more dramatic overshoot.
  • Always show the bubble (not just on press) by removing the transform: scale(0) default and the .active toggle logic, keeping only the position update.
  • Add a loan term selector (12/24/36/60 months) as a row of buttons that update const n in the payment formula, recalculating on click.

Gotchas — read before shipping

  • The bubble left position is a percentage of the track width — if the input has horizontal padding, the calculation must subtract the padding to stay aligned with the thumb; adjust with a padding offset variable.
  • The mouseup listener is on document, not the input — this is intentional so the bubble dismisses even if the user releases the mouse outside the component.
  • Spring cubic-bezier curves with control points outside 0–1 on the Y-axis (like the pop spring) can cause visual clipping in parent elements with overflow: hidden — ensure the bubble container has enough padding-top.

Browser support

ChromeSafariFirefoxEdge
80+14+78+80+

Spring cubic-bezier with Y values outside 0-1 is fully supported in all modern browsers for bubble pop animation.

Techniques used in this demo

Search CodeFronts

Loading…