Original by Plinth · MIT
Forms
Glass Toggle
A Liquid Glass switch whose translucent bead squashes against the wall as it springs across a lit trough.
#liquid-glass#toggle#switch#form#backdrop-filter
Install
npx shadcn@latest add https://plinthui.com/r/glass-toggle.json'use client';
import { useState } from 'react';
// Glass only reads against something behind it — place this over an image,
// a gradient or busy content; on a plain white background it will look
// nearly invisible. The material (backdrop-filter, a mask-composite rim,
// the pointer-driven highlight) is outside what Tailwind can express, so it
// lives in the embedded style element below.
const css = `
.glass-toggle-rows {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
/* ---------- the glass switch ---------- */
.glass-toggle {
position: relative;
display: inline-flex;
align-items: center;
gap: 14px;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* Hidden but still focusable, so the checkbox keeps its keyboard behaviour. */
.glass-toggle__input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
}
.glass-toggle__track {
position: relative;
isolation: isolate;
box-sizing: border-box;
flex: none;
width: 96px;
height: 52px;
border-radius: 999px;
background: linear-gradient(150deg, rgba(255, 255, 255, 0.28) 0%, rgba(255, 255, 255, 0.07) 48%, rgba(255, 255, 255, 0.18) 100%);
-webkit-backdrop-filter: blur(14px) saturate(190%) brightness(1.06);
backdrop-filter: blur(14px) saturate(190%) brightness(1.06);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.55),
inset 0 -1px 1px rgba(255, 255, 255, 0.22),
/* the trough: light gathering just inside the whole perimeter */
inset 0 3px 9px -5px rgba(3, 34, 32, 0.55),
inset 0 0 16px -6px rgba(255, 255, 255, 0.4),
0 1px 3px rgba(3, 34, 32, 0.22),
0 10px 22px -12px rgba(3, 34, 32, 0.45);
}
/* specular rim: bright where the light lands, gone along the shaded arc */
.glass-toggle__track::before {
content: "";
position: absolute;
inset: 0;
padding: 1.5px;
border-radius: inherit;
pointer-events: none;
z-index: 3;
background: linear-gradient(
150deg,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0.5) 18%,
rgba(255, 255, 255, 0.06) 44%,
rgba(255, 255, 255, 0.06) 62%,
rgba(255, 255, 255, 0.82) 100%
);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
/* the brass light that fills the trough when the switch is on */
.glass-toggle__fill {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
pointer-events: none;
opacity: 0;
background: linear-gradient(150deg, rgba(255, 226, 168, 0.82) 0%, rgba(255, 178, 84, 0.62) 100%);
box-shadow: inset 0 0 18px -4px rgba(255, 208, 120, 0.9);
transition: opacity 320ms ease;
}
/* the bead: translucent, so the light under it still comes through */
.glass-toggle__thumb {
position: absolute;
z-index: 2;
top: 6px;
left: 6px;
width: 40px;
height: 40px;
border-radius: 999px;
pointer-events: none;
background: linear-gradient(158deg, rgba(255, 255, 255, 0.62) 0%, rgba(255, 255, 255, 0.3) 48%, rgba(255, 255, 255, 0.5) 100%);
-webkit-backdrop-filter: blur(3px) saturate(220%) brightness(1.5);
backdrop-filter: blur(3px) saturate(220%) brightness(1.5);
box-shadow:
inset 0 2px 2px -1px rgba(255, 255, 255, 0.95),
inset 0 -2px 3px -1px rgba(255, 255, 255, 0.6),
inset 0 0 12px -4px rgba(255, 255, 255, 0.85),
0 2px 4px rgba(3, 34, 32, 0.3),
0 8px 16px -6px rgba(3, 34, 32, 0.45);
translate: 0;
/* Travel is the track's inner width minus the bead's own width, so it
can stretch while pressed without ever leaving the trough. */
transition:
translate 460ms cubic-bezier(0.34, 1.45, 0.5, 1),
width 240ms cubic-bezier(0.34, 1.45, 0.5, 1);
}
.glass-toggle__thumb::after {
content: "";
position: absolute;
left: 22%;
top: 13%;
width: 42%;
height: 26%;
border-radius: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0));
}
.glass-toggle__label {
font-size: 14px;
font-weight: 500;
letter-spacing: -0.005em;
color: rgba(255, 255, 255, 0.94);
text-shadow: 0 1px 3px rgba(3, 34, 32, 0.5);
}
.glass-toggle__input:checked ~ .glass-toggle__track .glass-toggle__fill { opacity: 1; }
.glass-toggle__input:checked ~ .glass-toggle__track .glass-toggle__thumb {
translate: calc(84px - 100%);
}
/* the squash: the bead widens against whichever wall it is resting on */
.glass-toggle:active .glass-toggle__thumb { width: 47px; }
.glass-toggle__input:focus-visible ~ .glass-toggle__track {
outline: 2px solid rgba(255, 255, 255, 0.92);
outline-offset: 3px;
}
`;
function Switch({
label,
checked,
onChange,
}: {
label: string;
checked: boolean;
onChange: (next: boolean) => void;
}) {
return (
<label className="glass-toggle">
{/* A real checkbox, hidden but still focusable, so the switch keeps its
keyboard behaviour and the CSS can key off :checked. */}
<input
className="glass-toggle__input"
type="checkbox"
role="switch"
checked={checked}
onChange={event => onChange(event.target.checked)}
/>
<span className="glass-toggle__track" aria-hidden="true">
<span className="glass-toggle__fill" />
<span className="glass-toggle__thumb" />
</span>
<span className="glass-toggle__label">{label}</span>
</label>
);
}
export function GlassToggle({
labels = ['Liquid Glass', 'Reduce motion'],
initial = [true, false],
}: {
labels?: string[];
initial?: boolean[];
}) {
const [state, setState] = useState(() => labels.map((_, i) => initial[i] ?? false));
return (
<>
<style>{css}</style>
<div className="glass-toggle-rows">
{labels.map((label, i) => (
<Switch
key={label}
label={label}
checked={state[i]}
onChange={next => setState(prev => prev.map((v, j) => (j === i ? next : v)))}
/>
))}
</div>
</>
);
}