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.
Install
npx shadcn@latest add https://plinthui.com/r/glass-card.json'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">
›
</span>
</div>
</article>
</>
);
}