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.

Live Demo
Plain CSSTailwind v4
Try it

Build interactive UI like this from scratch: 20 HTML, CSS & JavaScript projects

ZTM · JavaScript Web Projects · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

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…