Skip to content
Plinth

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
Open in v0
'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>
    </>
  );
}

More in Forms

Similar tags