Thermometer
import { useState } from 'react';
const css = `
@keyframes slider-thermometer-boil {
from { transform: translateY(0) scale(0.6); opacity: 0; }
30% { opacity: 1; }
to { transform: translateY(-16px) scale(1); opacity: 0; }
}
.slider-thermometer { display: flex; align-items: center; gap: 22px; }
.slider-thermometer__body { position: relative; width: 36px; height: 170px; }
.slider-thermometer__tube { position: absolute; left: 9px; top: 0; width: 18px; height: 140px; border-radius: 9px 9px 0 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1.5px light-dark(#cbced3, #393c41); }
.slider-thermometer__bulb { position: absolute; left: 0; bottom: 0; width: 36px; height: 36px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 1.5px light-dark(#cbced3, #393c41), 0 0 0 5px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6.5px light-dark(#cbced3, #393c41); }
.slider-thermometer__column {
position: absolute;
left: 14px;
bottom: 24px;
width: 8px;
height: calc(6px + var(--slider-thermometer-v) * 108px);
border-radius: 4px 4px 0 0;
background: light-dark(#373a40, #f1f2f3);
transition: height 0.45s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.slider-thermometer__bubble {
position: absolute;
bottom: 22px;
width: 4px;
height: 4px;
border-radius: 50%;
background: light-dark(#fbfcfd, #121417);
opacity: 0;
}
.slider-thermometer[data-hot] .slider-thermometer__bubble { animation: slider-thermometer-boil 0.9s ease-out infinite; }
.slider-thermometer__tick { position: absolute; left: 30px; height: 1px; width: 6px; background: light-dark(#6d7179, #7d8087); }
.slider-thermometer__tick--major { width: 10px; background: light-dark(#5f626a, #a7abb2); }
.slider-thermometer:has(.slider-thermometer__input:focus-visible) .slider-thermometer__tube { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-thermometer__input {
position: absolute;
left: 50%;
top: 75px;
width: 140px;
height: 36px;
margin: 0;
transform: translate(-50%, -50%) rotate(-90deg);
opacity: 0;
cursor: ns-resize;
-webkit-appearance: none;
appearance: none;
background: none;
}
.slider-thermometer__input::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 36px; }
.slider-thermometer__input::-moz-range-thumb { width: 12px; height: 36px; border: 0; }
.slider-thermometer__read { display: grid; gap: 2px; }
.slider-thermometer__deg { color: light-dark(#373a40, #f1f2f3); font-size: 38px; font-weight: 300; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; line-height: 1; }
.slider-thermometer__unit { color: light-dark(#5f626a, #a7abb2); font-size: 11px; font-weight: 600; letter-spacing: 0.08em; }
@media (prefers-reduced-motion: reduce) {
.slider-thermometer__column { transition: none; }
.slider-thermometer__bubble { display: none; }
}
`;
const TICKS = Array.from({ length: 11 }, (_, i) => i);
export function ThermometerSlider() {
const [v, setV] = useState(55);
const deg = Math.round(-10 + (v / 100) * 50);
return (
<>
<style>
{css}
</style>
<div className="slider-thermometer" data-hot={v >= 85 ? '' : undefined} style={{ '--slider-thermometer-v': v / 100 } as React.CSSProperties}>
<span className="slider-thermometer__body">
<span className="slider-thermometer__tube" aria-hidden="true" />
{TICKS.map((i) => (
<span key={i} className={`slider-thermometer__tick${i % 2 ? '' : ' slider-thermometer__tick--major'}`} style={{ bottom: 30 + i * 10.8 }} aria-hidden="true" />
))}
<span className="slider-thermometer__column" aria-hidden="true" />
<span className="slider-thermometer__bulb" aria-hidden="true" />
<span className="slider-thermometer__bubble" style={{ left: 12 }} aria-hidden="true" />
<span className="slider-thermometer__bubble" style={{ left: 20, animationDelay: '-0.3s' }} aria-hidden="true" />
<span className="slider-thermometer__bubble" style={{ left: 16, animationDelay: '-0.6s' }} aria-hidden="true" />
<input
className="slider-thermometer__input"
type="range"
min={0}
max={100}
value={v}
onChange={(e) => setV(+e.target.value)}
aria-label="Temperature"
aria-valuetext={`${deg} degrees Celsius`}
/>
</span>
<span className="slider-thermometer__read" aria-hidden="true">
<span className="slider-thermometer__deg">{deg}°</span>
<span className="slider-thermometer__unit">CELSIUS</span>
</span>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderThermometer(el) on the mounted element instead.
<div class="slider-thermometer" style="--slider-thermometer-v:0.55">
<span class="slider-thermometer__body">
<span class="slider-thermometer__tube" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:30px" aria-hidden="true"></span>
<span class="slider-thermometer__tick" style="bottom:40.8px" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:51.6px" aria-hidden="true"></span>
<span class="slider-thermometer__tick" style="bottom:62.400000000000006px" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:73.2px" aria-hidden="true"></span>
<span class="slider-thermometer__tick" style="bottom:84px" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:94.80000000000001px" aria-hidden="true"></span>
<span class="slider-thermometer__tick" style="bottom:105.60000000000001px" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:116.4px" aria-hidden="true"></span>
<span class="slider-thermometer__tick" style="bottom:127.2px" aria-hidden="true"></span>
<span class="slider-thermometer__tick slider-thermometer__tick--major" style="bottom:138px" aria-hidden="true"></span>
<span class="slider-thermometer__column" aria-hidden="true"></span>
<span class="slider-thermometer__bulb" aria-hidden="true"></span>
<span class="slider-thermometer__bubble" style="left:12px" aria-hidden="true"></span>
<span class="slider-thermometer__bubble" style="left:20px;animation-delay:-0.3s" aria-hidden="true"></span>
<span class="slider-thermometer__bubble" style="left:16px;animation-delay:-0.6s" aria-hidden="true"></span>
<input class="slider-thermometer__input" type="range" min="0" max="100" aria-label="Temperature" aria-valuetext="18 degrees Celsius" value="55"/>
</span>
<span class="slider-thermometer__read" aria-hidden="true">
<span class="slider-thermometer__deg">18°</span>
<span class="slider-thermometer__unit">CELSIUS</span>
</span>
</div>@keyframes slider-thermometer-boil {
from { transform: translateY(0) scale(0.6); opacity: 0; }
30% { opacity: 1; }
to { transform: translateY(-16px) scale(1); opacity: 0; }
}
.slider-thermometer { display: flex; align-items: center; gap: 22px; }
.slider-thermometer__body { position: relative; width: 36px; height: 170px; }
.slider-thermometer__tube { position: absolute; left: 9px; top: 0; width: 18px; height: 140px; border-radius: 9px 9px 0 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1.5px light-dark(#cbced3, #393c41); }
.slider-thermometer__bulb { position: absolute; left: 0; bottom: 0; width: 36px; height: 36px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 1.5px light-dark(#cbced3, #393c41), 0 0 0 5px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6.5px light-dark(#cbced3, #393c41); }
.slider-thermometer__column {
position: absolute;
left: 14px;
bottom: 24px;
width: 8px;
height: calc(6px + var(--slider-thermometer-v) * 108px);
border-radius: 4px 4px 0 0;
background: light-dark(#373a40, #f1f2f3);
transition: height 0.45s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.slider-thermometer__bubble {
position: absolute;
bottom: 22px;
width: 4px;
height: 4px;
border-radius: 50%;
background: light-dark(#fbfcfd, #121417);
opacity: 0;
}
.slider-thermometer[data-hot] .slider-thermometer__bubble { animation: slider-thermometer-boil 0.9s ease-out infinite; }
.slider-thermometer__tick { position: absolute; left: 30px; height: 1px; width: 6px; background: light-dark(#6d7179, #7d8087); }
.slider-thermometer__tick--major { width: 10px; background: light-dark(#5f626a, #a7abb2); }
.slider-thermometer:has(.slider-thermometer__input:focus-visible) .slider-thermometer__tube { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-thermometer__input {
position: absolute;
left: 50%;
top: 75px;
width: 140px;
height: 36px;
margin: 0;
transform: translate(-50%, -50%) rotate(-90deg);
opacity: 0;
cursor: ns-resize;
-webkit-appearance: none;
appearance: none;
background: none;
}
.slider-thermometer__input::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 36px; }
.slider-thermometer__input::-moz-range-thumb { width: 12px; height: 36px; border: 0; }
.slider-thermometer__read { display: grid; gap: 2px; }
.slider-thermometer__deg { color: light-dark(#373a40, #f1f2f3); font-size: 38px; font-weight: 300; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; line-height: 1; }
.slider-thermometer__unit { color: light-dark(#5f626a, #a7abb2); font-size: 11px; font-weight: 600; letter-spacing: 0.08em; }
@media (prefers-reduced-motion: reduce) {
.slider-thermometer__column { transition: none; }
.slider-thermometer__bubble { display: none; }
}function initSliderThermometer(root) {
var input = root.querySelector('.slider-thermometer__input');
var read = root.querySelector('.slider-thermometer__deg');
function update() {
var v = +input.value;
var deg = Math.round(-10 + (v / 100) * 50);
root.style.setProperty('--slider-thermometer-v', v / 100);
// The bulb bubbles near the top of the scale.
root.toggleAttribute('data-hot', v >= 85);
read.textContent = deg + '°';
input.setAttribute('aria-valuetext', deg + ' degrees Celsius');
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-thermometer').forEach(initSliderThermometer);Goal: Create a "Thermometer" slider.
Visual behavior: A temperature slider as a glass thermometer: the column climbs the scale and the reading changes in degrees; near the top, the bulb starts to bubble.
Technique:
- A real <input type="range"> lies over the drawing, transparent and rotated a quarter turn (-90deg), so dragging up raises the value and the arrow, Page Up/Down, Home and End keys all work natively. The drawing is aria-hidden.
- The value is written to one CSS custom property (0 to 1) on the root; the column height is calc() of it.
- A data-hot attribute from 85% starts the bubbles rising in the bulb, so they never show at rest.
- The reading maps the value onto -10 to 40 degrees Celsius.
Accessibility: A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the column jumps to the value and the bubbles are hidden.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Thermometer" slider.
Visual behavior: A temperature slider as a glass thermometer: the column climbs the scale and the reading changes in degrees; near the top, the bulb starts to bubble.
Technique:
- A real <input type="range"> lies over the drawing, transparent and rotated a quarter turn (-90deg), so dragging up raises the value and the arrow, Page Up/Down, Home and End keys all work natively. The drawing is aria-hidden.
- The value is written to one CSS custom property (0 to 1) on the root; the column height is calc() of it.
- A data-hot attribute from 85% starts the bubbles rising in the bulb, so they never show at rest.
- The reading maps the value onto -10 to 40 degrees Celsius.
Accessibility: A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the column jumps to the value and the bubbles are hidden.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A temperature slider as a glass thermometer: the column climbs the scale and the reading changes in degrees; near the top, the bulb starts to bubble.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- state change, fill, loop
- Techniques
- css variables, css transition, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the column jumps to the value and the bubbles are hidden.