Liquid Glass Slider
Well, it’s 2025 and Apple have seemingly decided to abandon accessible UI in favor of a Windows Vista-esque design language but without the opaque inner panels 😲.
So I better get on the bandwagon and make a liquid glass slider!!! 😰
Jokes aside, this is a fun demo of the slider component!
Technically-speaking, there
is no ‘liquid’ effect here, because cross-browsers don’t support the backdrop-filter,
filter, and mask properties simultaneously, they bug-out. (Also most of the liquid effects you
see are just a fractal noise displacement, not a real polar-coordinate liquid effect. I believe
a real effect is only possible with canvas or webgl and even then I think the background has to
be simulated and not real-time).
I’ve used some creative backdrop-filter: blur(n) and
mask properties to get a sort-of ‘liquid’ effect around the edges. By adding a psuedo-element
with a different blur-value—and masking it more than the handle—we then get two layers of blur
that work independently which gives the illusion of a liquid effect.
Adding on some creative transforms and transitions enhances the liquidey feeling.
<script>
let slider;
let classes = '';
let timer;
let isDarkMode = false;
/**
* when the slider handle moves, check if it's increasing or decreasing and
* add the appropriate class to the slider (we use css to animate the floating labels)
*/
const change = (e) => {
const delta = -(e.detail.previousValue - e.detail.value);
if ( delta > 0 ) { classes = 'up';}
else { classes = 'down'; }
clearTimeout( timer );
// end the animation when movement stops
timer = setTimeout( stop, 66 );
}
const stop = () => classes = '';
</script>
<RangeSlider
bind:slider
id="liquid-glass-slider"
class={classes}
darkmode={isDarkMode ? 'force' : false}
values={[30,70]}
range
pushy
rangeGapMin={20}
on:change={change}
on:stop={stop}
/>@property --handle-color {
syntax: '<color>';
inherits: false;
initial-value: white;
}
@property --handle-color-2 {
syntax: '<color>';
inherits: false;
initial-value: white;
}
.slider-container {
--bg-0: rgb(71, 57, 101);
--bg-1: url(/svelte-range-slider-pips/bgs/pexels-photo-9636386.webp) center center / 150%;
--bg-2: url(/svelte-range-slider-pips/bgs/pexels-photo-983200.webp) 75% -125% / 250%;
--bg-3: url(/svelte-range-slider-pips/bgs/pexels-photo-31427459.webp) center center / cover;
--bg-4: url(/svelte-range-slider-pips/bgs/pexels-photo-1704119.webp) center center / cover;
--bg-5: url(/svelte-range-slider-pips/bgs/pexels-photo-5417837.webp) center center / cover;
--bg-image: var(--bg-0);
background: var(--bg-image);
}
#liquid-glass-slider {
--slider: #ccc;
--slider-base: white;
--slider-accent: rgb(32, 230, 131);
--range: color-mix(in hsl, var(--slider-accent) 75%, white);
--ease-out: linear(
0,
0.002 0.5%,
0.008 1.1%,
0.017 1.6%,
0.031 2.2%,
0.049 2.8%,
0.07 3.4%,
0.098 4.1%,
0.129 4.8%,
0.184 5.9%,
0.257 7.2%,
0.551 12.1%,
0.671 14.2%,
0.735 15.4%,
0.789 16.5%,
0.839 17.6%,
0.881 18.6%,
0.923 19.7%,
0.957 20.7%,
0.99 21.8%,
1.019 22.9%,
1.043 24%,
1.063 25.1%,
1.08 26.2%,
1.094 27.4%,
1.107 29%,
1.114 30.7%,
1.116 32.5%,
1.112 34.5%,
1.105 36.1%,
1.095 37.9%,
1.041 45.8%,
1.018 49.9%,
1.008 52.1%,
1 54.4%,
0.994 56.7%,
0.99 59.1%,
0.987 62.3%,
0.987 65.9%,
0.999 84.9%,
1
);
--ease-in: linear(
0,
-0.001 4.2%,
-0.006 8.1%,
-0.014 12%,
-0.027 15.9%,
-0.041 19.5%,
-0.059 23.4%,
-0.124 35.9%,
-0.146 40.4%,
-0.164 45%,
-0.174 49%,
-0.177 51.5%,
-0.177 53.8%,
-0.174 56%,
-0.168 58.2%,
-0.16 60.3%,
-0.148 62.3%,
-0.134 64.3%,
-0.116 66.2%,
-0.09 68.6%,
-0.057 71%,
-0.02 73.3%,
0.024 75.6%,
0.072 77.8%,
0.127 80%,
0.189 82.2%,
0.255 84.3%,
0.405 88.4%,
0.58 92.4%,
0.775 96.2%,
1
);
--base-brightness: 1;
--base-contrast: 1;
margin-block: 5rem;
& .rangeHandle {
--handle-color: var(--slider-base);
--handle-color-2: color-mix(in hsl, var(--handle-color) 75%, black);
background: linear-gradient(to bottom, var(--handle-color), var(--handle-color-2));
width: 1.75em;
height: 1.25em;
border-radius: 2em;
top: 50%;
outline: none;
transition:
box-shadow 0.33s var(--ease-in),
--handle-color 0.33s var(--ease-in),
--handle-color-2 0.33s var(--ease-in),
--inset-color 0.33s var(--ease-in);
--shadow-opacity: 0.12;
--inset-color: rgba(0, 0, 0, 0);
box-shadow:
inset 0 0 0 1px var(--inset-color),
rgba(0, 0, 0, var(--shadow-opacity)) 0px 1px 2px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 2px 4px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 4px 8px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 8px 16px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 16px 32px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 32px 64px;
&.rsActive {
transition:
box-shadow 0.75s var(--ease-out),
--handle-color 0.75s var(--ease-out),
--handle-color-2 0.75s var(--ease-out),
--inset-color 0.75s var(--ease-out);
--shadow-opacity: 0.05;
--handle-color: var(--slider-accent);
--inset-color: color-mix(in hsl, var(--handle-color) 66%, black);
box-shadow:
inset 0 0 0 1px var(--inset-color),
rgba(0, 0, 0, var(--shadow-opacity)) 0px 1px 2px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 2px 4px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 4px 8px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 8px 16px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 16px 32px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 32px 64px;
}
& .rangeNub,
&::before,
&::after {
content: '';
position: absolute;
width: 5em;
height: 3.5em;
opacity: 0;
scale: 0.25 1;
transform: scaleY(0.33);
top: 50%;
left: 50%;
border-radius: 12em;
translate: -50% -50%;
transition: all 0.33s var(--ease-in);
will-change: opacity, scale, box-shadow, backdrop-filter, translate, transform;
box-shadow:
inset 1px 0.5px 0 1px rgba(255, 255, 255, 0.1),
inset -0.5px -1px 0 1px rgba(255, 255, 255, 0.1),
inset 3px 5px 4px -2px rgba(255, 255, 255, 0.15);
background: none;
}
&.rsActive .rangeNub,
&.rsActive::before,
&.rsActive::after {
opacity: 1;
scale: 1;
transform: scaleY(1);
transition:
all 0.75s var(--ease-out),
transform 0.7s var(--ease-out) 0.05s;
}
& .rangeNub {
mask-image: radial-gradient(ellipse, transparent calc(100% - 25px), black calc(100% + 10px));
backdrop-filter: blur(16px) brightness(calc(var(--base-brightness) * 2))
contrast(calc(var(--base-contrast) * 2)) blur(5px) saturate(1.5);
}
&::before {
box-shadow: none;
mask-image: radial-gradient(ellipse, transparent 33%, black calc(100% - 12px));
backdrop-filter: brightness(calc(var(--base-brightness) * 1.15))
contrast(calc(var(--base-contrast) * 1.33)) blur(3px)
brightness(calc(var(--base-brightness) * 1.2)) contrast(calc(var(--base-contrast) * 1.75))
saturate(1.2) blur(3px);
}
&::after {
mask: none;
backdrop-filter: none;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.33) 0%,
rgba(255, 255, 255, 0) 45%,
rgba(255, 255, 255, 0) 66%,
rgba(255, 255, 255, 0.2)
);
z-index: 3;
--shadow-opacity: 0.1;
box-shadow:
rgba(0, 0, 0, var(--shadow-opacity)) 0px 1px 2px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 1px 4px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 3px 8px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 6px 16px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 12px 32px,
rgba(0, 0, 0, var(--shadow-opacity)) 0px 26px 64px;
}
}
}
#liquid-glass-slider.up .rangeHandle,
#liquid-glass-slider.down .rangeHandle {
& .rangeNub,
&::before,
&::after {
scale: 1.2 0.925;
translate: -60% -50% 0;
}
}
#liquid-glass-slider.down .rangeHandle {
& .rangeNub,
&::before,
&::after {
translate: -40% -50% 0;
}
}
#liquid-glass-slider.rsDark {
--base-brightness: 0.85;
--base-contrast: 1.05;
}