Skip to content
Plinth

Original by Plinth · MIT

Cards

Glass Card

A Liquid Glass panel that refracts the colour behind it, rimmed with a specular hairline that follows your cursor.

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

Install

npx shadcn@latest add https://plinthui.com/r/glass-card.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-card {
  position: relative;
  isolation: isolate;
  box-sizing: border-box;
  width: 300px;
  padding: 20px 22px 18px;
  border-radius: 28px;
  background:
    linear-gradient(148deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.08) 46%, rgba(255, 255, 255, 0.18) 100%);
  -webkit-backdrop-filter: blur(16px) saturate(190%) brightness(1.12);
  backdrop-filter: blur(16px) saturate(190%) brightness(1.12);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.62),
    inset 0 -1px 1px rgba(255, 255, 255, 0.24),
    inset 0 0 22px -6px rgba(255, 255, 255, 0.4),
    inset 0 10px 22px -16px rgba(255, 255, 255, 0.5),
    0 2px 6px rgba(10, 10, 35, 0.18),
    0 16px 30px -12px rgba(10, 10, 35, 0.36),
    0 36px 64px -28px rgba(10, 10, 35, 0.5);
}
.glass-card::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 1.5px;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    146deg,
    rgba(255, 255, 255, 1) 0%,
    rgba(255, 255, 255, 0.55) 16%,
    rgba(255, 255, 255, 0.07) 40%,
    rgba(255, 255, 255, 0.07) 60%,
    rgba(255, 255, 255, 0.78) 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-card__sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    circle 240px at var(--mx) var(--my),
    rgba(255, 255, 255, 0.34),
    rgba(255, 255, 255, 0.07) 46%,
    transparent 72%
  );
}
.glass-card__eyebrow {
  position: relative;
  margin: 0;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.8);
  text-shadow: 0 1px 2px rgba(10, 12, 32, 0.3);
}
.glass-card__title {
  position: relative;
  margin: 8px 0 0;
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: #fff;
  text-shadow: 0 1px 2px rgba(10, 12, 32, 0.28);
}
.glass-card__text {
  position: relative;
  margin: 7px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.88);
  text-shadow: 0 1px 2px rgba(10, 12, 32, 0.3);
}
.glass-card__rule {
  position: relative;
  height: 1px;
  margin: 15px 0 12px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.06));
}
.glass-card__foot {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.82);
  text-shadow: 0 1px 2px rgba(10, 12, 32, 0.3);
}
.glass-card__chev { font-size: 15px; color: rgba(255, 255, 255, 0.6); }
`;

const REST = { mx: '26%', my: '-12%' };

export function GlassCard({
  eyebrow = 'Material',
  title = 'Liquid Glass',
  children = 'A translucent layer that bends the colour behind it, edged with a hairline of light.',
  footnote = 'Refracts what it sits on',
}: {
  eyebrow?: string;
  title?: string;
  children?: React.ReactNode;
  footnote?: string;
}) {
  const cardRef = useRef<HTMLElement>(null);

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

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

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

      <article
        ref={cardRef}
        className="glass-card"
        style={{ '--mx': REST.mx, '--my': REST.my } as React.CSSProperties}
        onPointerMove={handlePointerMove}
        onPointerLeave={() => setSheen(REST.mx, REST.my)}
      >
        <span className="glass-card__sheen" aria-hidden="true" />
        <p className="glass-card__eyebrow">{eyebrow}</p>
        <h3 className="glass-card__title">{title}</h3>
        <p className="glass-card__text">{children}</p>
        <div className="glass-card__rule" aria-hidden="true" />
        <div className="glass-card__foot">
          <span>{footnote}</span>
          <span className="glass-card__chev" aria-hidden="true">
            &rsaquo;
          </span>
        </div>
      </article>
    </>
  );
}

More in Cards

Similar tags