Skip to content
Plinth

Original by Plinth · MIT

Buttons

Glass Button

A Liquid Glass pill that refracts the colour beneath it, with a specular rim and a highlight that tracks your pointer.

#liquid-glass#button#backdrop-filter#specular#hover

Install

npx shadcn@latest add https://plinthui.com/r/glass-button.json
Open in v0
'use client';

import { useRef } 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-btn {
  position: relative;
  isolation: isolate;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 15px 30px;
  border: 0;
  border-radius: 999px;
  font: inherit;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: #fff;
  text-shadow: 0 1px 3px rgba(30, 8, 40, 0.5);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  background:
    radial-gradient(circle 130px at var(--mx) var(--my), rgba(255, 255, 255, 0.44), rgba(255, 255, 255, 0.07) 55%, transparent 76%),
    linear-gradient(148deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.09) 46%, rgba(255, 255, 255, 0.2) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(195%) brightness(1.12);
  backdrop-filter: blur(14px) saturate(195%) brightness(1.12);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.7),
    inset 0 -1px 1px rgba(255, 255, 255, 0.28),
    inset 0 0 18px -6px rgba(255, 255, 255, 0.45),
    0 2px 5px rgba(34, 8, 44, 0.22),
    0 10px 20px -8px rgba(34, 8, 44, 0.36),
    0 22px 40px -20px rgba(34, 8, 44, 0.5);
  transition:
    transform 220ms cubic-bezier(0.34, 1.4, 0.5, 1),
    box-shadow 260ms ease;
}
.glass-btn svg { width: 17px; height: 17px; display: block; }
.glass-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 1.5px;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(
    148deg,
    rgba(255, 255, 255, 1) 0%,
    rgba(255, 255, 255, 0.55) 18%,
    rgba(255, 255, 255, 0.07) 44%,
    rgba(255, 255, 255, 0.07) 62%,
    rgba(255, 255, 255, 0.85) 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;
}
.glass-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  background: linear-gradient(102deg, transparent 34%, rgba(255, 255, 255, 0.55) 50%, transparent 66%);
  background-size: 260% 100%;
  background-position: 150% 0;
}
.glass-btn:hover {
  transform: translateY(-1.5px);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.85),
    inset 0 -1px 1px rgba(255, 255, 255, 0.34),
    inset 0 0 20px -5px rgba(255, 255, 255, 0.6),
    0 3px 7px rgba(34, 8, 44, 0.24),
    0 14px 26px -8px rgba(34, 8, 44, 0.4),
    0 30px 52px -22px rgba(34, 8, 44, 0.55);
}
.glass-btn:hover::after {
  animation: glass-btn-sweep 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes glass-btn-sweep {
  0%   { opacity: 0; background-position: 150% 0; }
  18%  { opacity: 1; }
  100% { opacity: 0; background-position: -70% 0; }
}
.glass-btn:active {
  transform: translateY(0) scale(0.972);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.5),
    inset 0 6px 14px -8px rgba(30, 8, 40, 0.42),
    inset 0 0 14px -6px rgba(255, 255, 255, 0.3),
    0 1px 3px rgba(34, 8, 44, 0.24),
    0 5px 12px -6px rgba(34, 8, 44, 0.3);
  transition: transform 90ms ease-out, box-shadow 90ms ease-out;
}
.glass-btn:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.95);
  outline-offset: 3px;
}
`;

const REST = { mx: '30%', my: '0%' };

export function GlassButton({
  children = 'Continue',
  onClick,
}: {
  children?: React.ReactNode;
  onClick?: () => void;
}) {
  const btnRef = useRef<HTMLButtonElement>(null);

  // Written straight to the element so the pointer never triggers a re-render.
  const setHighlight = (x: string, y: string) => {
    btnRef.current?.style.setProperty('--mx', x);
    btnRef.current?.style.setProperty('--my', y);
  };

  const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
    const rect = event.currentTarget.getBoundingClientRect();
    setHighlight(`${event.clientX - rect.left}px`, `${event.clientY - rect.top}px`);
  };

  return (
    <>
      <style>{css}</style>

      <button
        ref={btnRef}
        type="button"
        className="glass-btn"
        style={{ '--mx': REST.mx, '--my': REST.my } as React.CSSProperties}
        onPointerMove={handlePointerMove}
        onPointerLeave={() => setHighlight(REST.mx, REST.my)}
        onClick={onClick}
      >
        {children}
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          aria-hidden="true"
        >
          <path d="M4.5 12h14" />
          <path d="m12.8 5.8 6.2 6.2-6.2 6.2" />
        </svg>
      </button>
    </>
  );
}

More in Buttons

Similar tags