Back to Components
Interactive Glass Thermostat UI with GSAP & SVG Effects
Component

Interactive Glass Thermostat UI with GSAP & SVG Effects

CodewithLord
December 20, 2025

A futuristic glassmorphism-based thermostat UI built with HTML, CSS, SVG filters, and GSAP. Features a draggable temperature knob, plasma-like mercury animation, dynamic color gradients, animated scale markings, and snow particle effects below freezing.

Description


This project implements a high-end interactive glass thermostat UI inspired by physical temperature gauges and futuristic control panels.
It combines glassmorphism, SVG displacement filters, and GSAP-powered interactions to create a tactile, visually rich experience.

Key highlights:

  • Draggable temperature knob with inertia
  • Plasma-like mercury fill using SVG turbulence
  • Dynamic gradient-based temperature coloring
  • Animated scale with focus magnification
  • Snow particle system when temperature drops below 40°F
  • Real-time status updates (Cold, Warm, Hot, etc.)
  • Fully responsive and GPU-accelerated animations

This component is ideal for dashboard UIs, IoT mockups, smart home concepts, and portfolio showcases.


HTML

1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Glass Thermostat</title> 7 <link rel="stylesheet" href="./style.css"> 8 9 </head> 10 11 <body> 12 <!-- Credit --> 13<!-- The existence of this pen wouldn't've been possible without the following --> 14<!-- https://codepen.io/ash_creator/pen/zYaPZLB --> 15<!-- https://codepen.io/BalintFerenczy/pen/KwdoyEN --> 16<!-- https://www.perplexity.ai/ --> 17<!-- https://aistudio.google.com/ --> 18 19<!-- Snow appears when the temperature drops below 40°F. --> 20 21<svg style="position:absolute; width:0; height:0;"> 22 <defs> 23 <filter id="turbulent-displace" colorInterpolationFilters="sRGB" x="-20%" y="-20%" width="140%" height="140%"> 24 <feTurbulence type="turbulence" baseFrequency="0.02" numOctaves="10" result="noise1" seed="1" /> 25 <feOffset in="noise1" dx="0" dy="0" result="offsetNoise1"> 26 <animate attributeName="dy" values="700; 0" dur="6s" repeatCount="indefinite" calcMode="linear" /> 27 </feOffset> 28 <feTurbulence type="turbulence" baseFrequency="0.02" numOctaves="10" result="noise2" seed="1" /> 29 <feOffset in="noise2" dx="0" dy="0" result="offsetNoise2"> 30 <animate attributeName="dy" values="0; -700" dur="6s" repeatCount="indefinite" calcMode="linear" /> 31 </feOffset> 32 <feTurbulence type="turbulence" baseFrequency="0.02" numOctaves="10" result="noise3" seed="2" /> 33 <feOffset in="noise3" dx="0" dy="0" result="offsetNoise3"> 34 <animate attributeName="dx" values="490; 0" dur="6s" repeatCount="indefinite" calcMode="linear" /> 35 </feOffset> 36 <feTurbulence type="turbulence" baseFrequency="0.02" numOctaves="10" result="noise4" seed="2" /> 37 <feOffset in="noise4" dx="0" dy="0" result="offsetNoise4"> 38 <animate attributeName="dx" values="0; -490" dur="6s" repeatCount="indefinite" calcMode="linear" /> 39 </feOffset> 40 <feComposite in="offsetNoise1" in2="offsetNoise2" result="part1" /> 41 <feComposite in="offsetNoise3" in2="offsetNoise4" result="part2" /> 42 <feBlend in="part1" in2="part2" mode="color-dodge" result="combinedNoise" /> 43 <feDisplacementMap in="SourceGraphic" in2="combinedNoise" scale="30" xChannelSelector="R" yChannelSelector="B" /> 44 </filter> 45 </defs> 46</svg> 47 48<div id="app"> 49 <div class="thermostat-ui"> 50 <div class="thermostat glass-panel"> 51 <div class="thermostat-inner"> 52 <div class="glass-noise"></div> 53 <div class="scale-container" id="scaleContainer"></div> 54 <div class="track" id="track"> 55 <div class="mercury" id="mercury"></div> 56 </div> 57 <div class="knob-zone"> 58 <div class="knob" id="knob"></div> 59 </div> 60 </div> 61 </div> 62 <div class="temp-readout"> 63 <div class="temp-value" id="tempValue">70°</div> 64 <div class="temp-label">CURRENT TEMP</div> 65 <div class="status-text" id="statusText">Comfortable</div> 66 </div> 67 </div> 68</div> 69<div class="particles-container" id="uiParticles"></div> 70 <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js'></script> 71<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/Draggable.min.js'></script><script src="./script.js"></script> 72 73 </body> 74 75</html>

🎨 CSS Code



1:root { 2 --glass-bg: rgba(10, 10, 10, 0.7); 3 --glass-border: rgba(255, 255, 255, 0.08); 4 --glow-color: #00a2fa; 5} 6 7/* Global reset */ 8* { 9 box-sizing: border-box; 10} 11 12html, body { 13 margin: 0; 14 padding: 0; 15 height: 100vh; 16 background: #000; 17 color: #fff; 18 overflow: hidden; 19 font-family: "Inter", system-ui, sans-serif; 20} 21 22#app { 23 height: 100vh; 24 display: flex; 25 align-items: center; 26 justify-content: center; 27} 28 29/* Glass body */ 30.glass-panel { 31 background: var(--glass-bg); 32 backdrop-filter: blur(20px) saturate(180%); 33 -webkit-backdrop-filter: blur(20px) saturate(180%); 34 border: 1px solid var(--glass-border); 35 box-shadow: 0 8px 32px rgba(0,0,0,0.6); 36} 37 38.thermostat-ui { 39 position: relative; 40 display: flex; 41 flex-direction: column; 42 align-items: center; 43 gap: 28px; 44} 45 46/* Main body */ 47.thermostat { 48 position: relative; 49 width: 150px; 50 height: 520px; 51 border-radius: 999px; 52 overflow: visible; 53} 54 55.thermostat-inner { 56 position: relative; 57 width: 100%; 58 height: 100%; 59 border-radius: inherit; 60 overflow: visible; 61} 62 63.thermostat-inner::before { 64 content: ""; 65 position: absolute; 66 inset: 0; 67 border-radius: inherit; 68 border: 1px solid rgba(255,255,255,0.1); 69 mix-blend-mode: soft-light; 70 pointer-events: none; 71} 72 73/* Texture */ 74.glass-noise { 75 position: absolute; 76 inset: 0; 77 border-radius: inherit; 78 opacity: 0.08; 79 mix-blend-mode: overlay; 80 pointer-events: none; 81 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); 82} 83 84/* Track */ 85.track { 86 position: absolute; 87 top: 46px; 88 bottom: 46px; 89 left: 50%; 90 transform: translateX(-50%); 91 width: 42px; 92 border-radius: 999px; 93 background: 94 radial-gradient(circle at 50% 0%, rgba(255,255,255,0.35) 0, transparent 55%), 95 radial-gradient(circle at 50% 100%, rgba(0,0,0,1) 0, rgba(0,0,0,0.9) 70%), 96 linear-gradient(180deg, rgba(255,255,255,0.04), rgba(0,0,0,0.8)); 97 background-blend-mode: screen, normal, soft-light; 98 box-shadow: inset 0 0 18px rgba(0,0,0,1), 0 0 18px rgba(0,0,0,0.8); 99 overflow: hidden; 100} 101 102/* Electric plasma fill */ 103.mercury { 104 position: absolute; 105 bottom: 0; 106 left: -45%; 107 width: 190%; 108 height: 0%; 109 background: var(--glow-color); 110 filter: url(#turbulent-displace); 111 mix-blend-mode: screen; 112 box-shadow: 0 0 45px var(--glow-color), 0 0 90px var(--glow-color); 113 transition: height 0.12s linear, box-shadow 0.3s ease, background 0.25s ease; 114 opacity: 0.95; 115} 116 117/* Flowing current over plasma */ 118.mercury::before, 119.mercury::after { 120 content: ""; 121 position: absolute; 122 inset: 0; 123 border-radius: inherit; 124 filter: blur(6px); 125 background: radial-gradient(circle at 50% 50%, rgba(255,255,255,0.3), transparent 90%); 126 mix-blend-mode: color-dodge; 127 opacity: 0.25; 128 animation: pulseElectric 3s infinite ease-in-out alternate; 129} 130 131.mercury::after { 132 filter: blur(16px); 133 opacity: 0.18; 134 animation-delay: 1.5s; 135} 136 137@keyframes pulseElectric { 138 0% { opacity: 0.15; transform: scaleY(1); } 139 100% { opacity: 0.35; transform: scaleY(1.05); } 140} 141 142/* Knob */ 143.knob-zone { 144 position: absolute; 145 top: 46px; 146 bottom: 46px; 147 left: 0; 148 right: 0; 149 pointer-events: none; 150} 151 152.knob { 153 position: absolute; 154 left: 50%; 155 transform: translate(-50%, -50%); 156 width: 72px; 157 height: 72px; 158 border-radius: 999px; 159 background: rgba(10,10,10,0.7); 160 backdrop-filter: blur(12px) saturate(260%) brightness(1.25); 161 -webkit-backdrop-filter: blur(12px) saturate(260%) brightness(1.25); 162 border: 1px solid rgba(255,255,255,0.14); 163 box-shadow: inset 0 1px 18px rgba(255,255,255,0.15), 0 8px 26px rgba(0,0,0,0.9); 164 cursor: grab; 165 pointer-events: auto; 166 transition: box-shadow 0.2s ease, transform 0.15s ease; 167} 168 169.knob:active { 170 transform: translate(-50%, -50%) scale(1.05); 171} 172 173/* Scale container and marks */ 174.scale-container { 175 position: absolute; 176 top: 46px; 177 bottom: 46px; 178 left: -90px; 179 width: 80px; 180 pointer-events: none; 181} 182 183.scale-mark { 184 position: absolute; 185 right: 0; 186 font-size: 14px; 187 color: rgba(255,255,255,0.35); 188 font-weight: 500; 189 display: flex; 190 align-items: center; 191 gap: 6px; 192 transform-origin: right center; 193 transition: all 0.1s ease; 194} 195 196.tick { 197 height: 2px; 198 background: rgba(255,255,255,0.4); 199 border-radius: 2px; 200 flex-shrink: 0; 201} 202 203/* Temperature readout */ 204.temp-readout { 205 text-align: center; 206} 207 208.temp-value { 209 font-size: 5.2rem; 210 font-weight: 700; 211 text-shadow: 0 0 48px var(--glow-color); 212 color: var(--glow-color); 213} 214 215.temp-label { 216 font-size: 0.9rem; 217 text-transform: uppercase; 218 letter-spacing: 0.34em; 219 opacity: 0.7; 220 margin-top: 10px; 221} 222 223.status-text { 224 margin-top: 8px; 225 font-size: 1.1rem; 226 text-transform: uppercase; 227 letter-spacing: 0.22em; 228 color: var(--glow-color); 229 opacity: 0.95; 230} 231 232/* More Prominent Snow Particles - full canvas */ 233.particles-container { 234 position: fixed; 235 inset: 0; 236 pointer-events: none; 237 z-index: 0; 238 overflow: hidden; 239} 240 241/* Responsive tweak */ 242@media (max-width: 480px){ 243 .thermostat { transform: scale(0.9);} 244} 245 246#app { z-index: 10; position: relative; } 247.thermostat-ui, .thermostat { z-index: 10; position: relative; } 248.particles-container { z-index: 1; } /* snow stays behind */

Javascript Code


1const CONFIG = { 2 minTemp: 20, 3 maxTemp: 110, 4 defaultTemp: 70, 5 gradientColors: ["#00eaff","#0099ff","#00ff73","#ffdd00","#ff8800","#ff0044"], 6 gradientStops: [0,0.25,0.5,0.7,0.85,1], 7 thresholds: { snow: 40 } 8}; 9 10const els = { 11 track: document.getElementById("track"), 12 mercury: document.getElementById("mercury"), 13 knob: document.getElementById("knob"), 14 scaleContainer: document.getElementById("scaleContainer"), 15 tempValue: document.getElementById("tempValue"), 16 statusText: document.getElementById("statusText"), 17 uiParticles: document.getElementById("uiParticles"), 18 root: document.documentElement 19}; 20 21let currentTemp = CONFIG.defaultTemp; 22let trackHeight = 0, knobBounds = { minY: 0, maxY: 0 }, scaleItems = [], colorMap; 23let snowParticleIntervalId = null; 24 25// linear interpolation helper 26const lerp = (a,b,t) => a + (b - a) * t; 27 28function createColorMap() { 29 const stops = CONFIG.gradientStops; 30 const colors = CONFIG.gradientColors.map(c => gsap.utils.splitColor(c)); 31 return t => { 32 t = Math.max(0, Math.min(1, t)); 33 for (let i=0; i<stops.length-1; i++) { 34 const s0 = stops[i], s1 = stops[i+1]; 35 if (t >= s0 && t <= s1) { 36 const n = (t - s0) / (s1 - s0); 37 const c0 = colors[i], c1 = colors[i+1]; 38 return `rgb(${Math.round(lerp(c0[0],c1[0],n))},${Math.round(lerp(c0[1],c1[1],n))},${Math.round(lerp(c0[2],c1[2],n))})`; 39 } 40 } 41 }; 42} 43 44function buildScale() { 45 els.scaleContainer.innerHTML = ""; 46 scaleItems = []; 47 const min = CONFIG.minTemp, max = CONFIG.maxTemp, range = max - min; 48 const rect = els.track.getBoundingClientRect(); 49 const trackH = rect.height; 50 for (let t = min; t <= max; t += 2) { 51 const el = document.createElement("div"); 52 el.className = "scale-mark"; 53 const tick = document.createElement("div"); 54 tick.className = "tick"; 55 if (t % 10 === 0) tick.style.width = "18px"; 56 else if (t % 5 === 0) tick.style.width = "12px"; 57 else tick.style.width = "6px"; 58 const y = (1 - (t - min) / range) * (trackH - 1); 59 el.style.top = `${y}px`; 60 if (t % 10 === 0) el.innerHTML = `${t}<div class="tick"></div>`; 61 el.appendChild(tick); 62 el.dataset.temp = t; 63 els.scaleContainer.appendChild(el); 64 scaleItems.push(el); 65 } 66} 67 68function updateScaleVisuals(knobY){ 69 scaleItems.forEach(el => { 70 const rect = els.track.getBoundingClientRect(); 71 const elY = parseFloat(el.style.top); 72 const dist = Math.abs(knobY - elY), maxDist = 70; 73 if (dist < maxDist) { 74 const p = 1 - dist / maxDist; 75 gsap.set(el, { 76 scale: 1 + p * 0.8, 77 opacity: 0.6 + p * 0.6, 78 color: "#fff", 79 textShadow: "0 0 8px var(--glow-color)" 80 }); 81 } else { 82 gsap.set(el, { 83 scale: 1, 84 opacity: 0.3, 85 color: "rgba(255,255,255,0.35)", 86 textShadow: "none" 87 }); 88 } 89 }); 90} 91 92function updateStatusText(t){ 93 let txt = ""; 94 if(t < 32) txt = "Freezing"; 95 else if(t < 55) txt = "Cold"; 96 else if(t < 66) txt = "Cool"; 97 else if(t <= 74) txt = "Comfortable"; 98 else if(t < 85) txt = "Warm"; 99 else if(t < 95) txt = "Hot"; 100 else txt = "Extreme"; 101 els.statusText.textContent = txt; 102} 103 104function applyColorTheme(color){ 105 els.root.style.setProperty("--glow-color", color); 106 els.tempValue.style.color = color; 107 els.statusText.style.color = color; 108 els.mercury.style.boxShadow = `0 0 40px ${color}, 0 0 80px ${color}`; 109} 110 111function updateSystemFromY(yPos){ 112 yPos = Math.max(knobBounds.minY, Math.min(knobBounds.maxY, yPos)); 113 const pct = 1 - yPos / trackHeight; 114 const temp = CONFIG.minTemp + pct * (CONFIG.maxTemp - CONFIG.minTemp); 115 currentTemp = Math.round(temp); 116 const norm = (currentTemp - CONFIG.minTemp) / (CONFIG.maxTemp - CONFIG.minTemp); 117 const color = colorMap(norm); 118 els.tempValue.textContent = currentTemp + "°"; 119 els.mercury.style.height = pct * 100 + "%"; 120 applyColorTheme(color); 121 updateStatusText(currentTemp); 122 updateScaleVisuals(yPos); 123 updateSnowParticles(currentTemp); 124} 125 126function initLayout(){ 127 const rect = els.track.getBoundingClientRect(); 128 trackHeight = rect.height; 129 knobBounds = { minY: 0, maxY: trackHeight }; 130 buildScale(); 131 const norm = (CONFIG.defaultTemp - CONFIG.minTemp) / (CONFIG.maxTemp - CONFIG.minTemp); 132 const startY = trackHeight * (1 - norm); 133 gsap.set(els.knob, { y: startY }); 134 updateSystemFromY(startY); 135} 136 137function initDrag(){ 138 Draggable.create(els.knob,{ 139 type: "y", 140 bounds: { minY: knobBounds.minY, maxY: knobBounds.maxY }, 141 inertia: true, 142 onDrag() { updateSystemFromY(this.y); }, 143 onThrowUpdate() { updateSystemFromY(this.y); } 144 }); 145} 146 147/* Improved Snow Particles with more prominence and random fall */ 148 149const minSnowSpawnInterval = 1.2; 150const maxSnowSpawnInterval = 5; 151const minSnowFallDuration = 4; 152const maxSnowFallDuration = 7; 153 154function createSnowParticle() { 155 const p = document.createElement("div"); 156 p.className = "particle"; 157 els.uiParticles.appendChild(p); 158 159 const vw = window.innerWidth, vh = window.innerHeight; 160 const size = Math.random() * 12 + 7; 161 const baseOpacity = 0.9 + Math.random() * 0.1; 162 const blurVal = Math.random() * 1.6 + 0.9; 163 164 p.style.width = p.style.height = size + "px"; 165 p.style.borderRadius = "50%"; 166 p.style.background = `radial-gradient(circle, rgba(255,255,255,${baseOpacity}) 0%, rgba(255,255,255,${baseOpacity*0.9}) 70%, transparent 100%)`; 167 p.style.filter = `blur(${blurVal}px)`; // +30% brightness 168 p.style.boxShadow = "0 0 24px rgba(255,255,255,0.95)"; // 30% bigger glow 169 170 // ALWAYS spawn ABOVE viewport 171 const startX = Math.random() * vw; 172 const startY = -50 - Math.random() * 150; // -50 to -200px 173 174 gsap.set(p, { x: startX, y: startY, opacity: 0, scale: 0.6 }); 175 176 const swayX = 80 + Math.random() * 60; 177 const fallDuration = getSnowFallDuration(currentTemp); 178 179 gsap.timeline({ onComplete: () => p.remove() }) 180 .to(p, { opacity: 1, scale: 1, duration: 0.7, ease: "power2.out" }) 181 .to(p, { 182 y: vh + 80, 183 x: "+=" + (Math.random() * swayX - swayX/2), 184 rotation: Math.random() * 180, 185 opacity: 0, 186 duration: fallDuration, 187 ease: "none" 188 }, 0) 189 .to(p, { 190 x: "+=" + (Math.random() * 40 - 20), 191 yoyo: true, 192 repeat: 1, 193 duration: 2 + Math.random() * 3, 194 ease: "sine.inOut" 195 }, 0.2); 196} 197 198function getSnowFallDuration(temp) { 199 const clampedTemp = Math.max(20, Math.min(temp, 40)); 200 const requiredPct = (40 - clampedTemp) / 20; 201 return lerp(maxSnowFallDuration, maxSnowFallDuration * 0.55, requiredPct); // 30% faster 202} 203 204function getSnowSpawnInterval(temp) { 205 const clampedTemp = Math.max(20, Math.min(temp, 40)); 206 const requiredPct = (40 - clampedTemp) / 20; 207 return lerp(maxSnowSpawnInterval, maxSnowSpawnInterval * 0.45, requiredPct); // 55% more snow 208} 209 210function updateSnowParticles(temp) { 211 if (temp > CONFIG.thresholds.snow) { 212 // Stop snow if above threshold 213 if (snowParticleIntervalId !== null) { 214 clearInterval(snowParticleIntervalId); 215 snowParticleIntervalId = null; 216 } 217 els.uiParticles.innerHTML = ""; 218 return; 219 } 220 221 if (snowParticleIntervalId !== null) clearInterval(snowParticleIntervalId); 222 223 // Spawn immediately on threshold crossing 224 createSnowParticle(); 225 226 const spawnInterval = getSnowSpawnInterval(temp) * 100; // convert to ms scaled 227 snowParticleIntervalId = setInterval(() => { 228 createSnowParticle(); 229 }, spawnInterval); 230} 231 232window.addEventListener("load", () => { 233 colorMap = createColorMap(); 234 initLayout(); 235 initDrag(); 236 updateSnowParticles(currentTemp); 237}); 238 239window.addEventListener("resize", () => { 240 initLayout(); 241 updateSnowParticles(currentTemp); 242});

How It Works


  1. Glassmorphism Structure

Frosted glass effect using backdrop-filter

Noise texture overlay for realism

Soft lighting and glow shadows

  1. Temperature Interaction

GSAP Draggable controls the knob

Knob Y-position maps to temperature range

Mercury fill height updates in real time


  1. Dynamic Color System

Gradient-based color interpolation

Glow color updates across UI

Temperature text and plasma stay in sync

  1. Environmental Feedback

Status text updates (Cold → Comfortable → Hot)

Snow particle system activates below 40°F

Particle density increases as temperature drops

Key Features


❄️ Temperature-aware snow particles

🧊 Glassmorphism UI design

🔥 Plasma-style mercury animation

🎛️ Smooth draggable knob with inertia

🎨 Dynamic color gradients

⚡ GPU-accelerated animations

📱 Responsive and scalable

Use Cases


Smart thermostat UI concepts

IoT dashboard mockups

Sci-fi control panels

Interactive product demos

Portfolio showcase components

Love this component?

Explore more components and build amazing UIs.

View All Components