Viaje al norte
Una experiencia fluida, pensada para que todo se sienta ligero.
Tarjeta de cristal
Una tarjeta esmerilada que se inclina en 3D hacia el cursor mientras un reflejo suave lo sigue.
Ajustes
Código
import { type CSSProperties, type MouseEvent, type ReactNode } from "react";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { Heart, Sparkles } from "lucide-react";
type Wallpaper = "aurora" | "sunset" | "ocean" | "light";
const spring = { type: "spring" as const, stiffness: 400, damping: 30 };
const walls: Record<Wallpaper, { base: string; blobs: string[] }> = {
aurora: { base: "#0b0b1a", blobs: ["#5b21b6", "#2563eb", "#ec4899"] },
sunset: { base: "#1a0b14", blobs: ["#f97316", "#f43f5e", "#8b5cf6"] },
ocean: { base: "#031a1f", blobs: ["#06b6d4", "#3b82f6", "#10b981"] },
light: { base: "#f8fafc", blobs: ["#fed7aa", "#ddd6fe", "#bae6fd"] },
};
const blobPositions = [
["-10%", "-15%"],
["45%", "5%"],
["5%", "50%"],
];
const grain = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")";
const glassCss = `
.lg-font {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif;
-webkit-font-smoothing: antialiased;
}
.lg-glass {
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, var(--lg-a, 0.12));
-webkit-backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -1px 0 rgba(255, 255, 255, 0.1),
inset 0 0 20px rgba(255, 255, 255, 0.08),
0 8px 32px rgba(0, 0, 0, 0.18);
}
/* Borde de luz */
.lg-glass::before {
content: "";
position: absolute;
inset: 0;
z-index: 2;
padding: 1px;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.05) 40%, rgba(255, 255, 255, 0.05) 60%, rgba(255, 255, 255, 0.4));
mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
/* Reflejo que sigue al cursor (--x / --y los pone track()) */
.lg-glass::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
pointer-events: none;
opacity: var(--lg-hover, 0);
transition: opacity 0.3s;
background: radial-gradient(circle 140px at var(--x, 50%) var(--y, 0%), rgba(255, 255, 255, var(--lg-shine, 0.25)), transparent 70%);
}
.lg-glass:hover {
--lg-hover: 1;
}
.lg-text {
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
.lg-light .lg-text {
color: #1d1d1f;
text-shadow: none;
}
@keyframes lg-drift {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(8%, -6%) scale(1.12); }
66% { transform: translate(-6%, 7%) scale(0.94); }
}
`;
// Guarda la posición del cursor para el reflejo de .lg-glass::after
function track(e: MouseEvent<HTMLElement>) {
const r = e.currentTarget.getBoundingClientRect();
e.currentTarget.style.setProperty("--x", `${e.clientX - r.left}px`);
e.currentTarget.style.setProperty("--y", `${e.clientY - r.top}px`);
}
interface StageProps {
bg: Wallpaper;
blur: number;
opacity: number;
shine: number;
className?: string;
children: ReactNode;
}
// Fondo de degradado animado sobre el que se ve el cristal
function Stage({
bg,
blur,
opacity,
shine,
className = "relative h-[560px] w-full",
children,
}: StageProps) {
const light = bg === "light";
const wall = walls[bg] ?? walls.aurora;
const vars = {
"--lg-blur": `${blur}px`,
"--lg-a": light ? Math.min(0.6, opacity / 100 + 0.23) : opacity / 100,
"--lg-shine": shine / 100,
} as CSSProperties;
return (
<div
className={`lg-font overflow-hidden rounded-[22px] ${className} ${light ? "lg-light" : ""}`}
style={vars}
>
<style>{glassCss}</style>
<div className="absolute inset-0 overflow-hidden" style={{ background: wall.base }}>
<div className="absolute inset-0" style={{ filter: "blur(60px)" }}>
{wall.blobs.map((color, i) => (
<div
key={i}
className="absolute h-[75%] w-[75%] rounded-full"
style={{
left: blobPositions[i]?.[0],
top: blobPositions[i]?.[1],
background: `radial-gradient(circle, ${color} 0%, transparent 68%)`,
opacity: light ? 1 : 0.9,
animation: `lg-drift 20s ease-in-out ${-i * 6}s infinite`,
}}
/>
))}
</div>
<div
className="absolute inset-0"
style={{ backgroundImage: grain, opacity: 0.04, mixBlendMode: "overlay" }}
/>
</div>
<div className="relative grid h-full w-full grid-cols-[100%] place-items-center p-4">{children}</div>
</div>
);
}
interface GlassCardProps {
label?: string;
bg?: Wallpaper;
blur?: number;
opacity?: number;
shine?: number;
}
export default function GlassCard({
label = "Viaje al norte",
bg = "aurora",
blur = 20,
opacity = 12,
shine = 25,
}: GlassCardProps) {
const glass = { bg, blur, opacity, shine };
const px = useMotionValue(0.5);
const py = useMotionValue(0.5);
const rotateX = useSpring(useTransform(py, [0, 1], [8, -8]), spring);
const rotateY = useSpring(useTransform(px, [0, 1], [-8, 8]), spring);
const onMove = (e: MouseEvent<HTMLDivElement>) => {
track(e);
const r = e.currentTarget.getBoundingClientRect();
px.set((e.clientX - r.left) / r.width);
py.set((e.clientY - r.top) / r.height);
};
return (
<Stage {...glass}>
<div style={{ perspective: 1000 }}>
<motion.div
onMouseMove={onMove}
onMouseLeave={() => {
px.set(0.5);
py.set(0.5);
}}
style={{ rotateX, rotateY, width: 320, height: 420, borderRadius: 32 }}
className="lg-glass lg-text flex max-w-[calc(100vw-48px)] flex-col p-7"
>
<div
className="relative z-[3] grid h-16 w-16 place-items-center rounded-[20px]"
style={{ background: "rgba(255,255,255,.18)", boxShadow: "inset 0 1px 0 rgba(255,255,255,.5)" }}
>
<Sparkles className="h-8 w-8" strokeWidth={1.5} />
</div>
<div className="relative z-[3] mt-auto">
<h3 className="text-[28px] font-bold leading-tight tracking-[-0.02em]">{label}</h3>
<p className="mt-1.5 text-[15px] opacity-70">
Una experiencia fluida, pensada para que todo se sienta ligero.
</p>
<motion.button
type="button"
onMouseMove={track}
whileTap={{ scale: 0.95 }}
transition={spring}
className="lg-glass mt-6 h-12 w-full rounded-full text-[16px] font-semibold"
>
<span className="relative z-[3] inline-flex items-center gap-2">
<Heart className="h-4 w-4" strokeWidth={1.75} />
Descubrir
</span>
</motion.button>
</div>
</motion.div>
</div>
</Stage>
);
}
Prompt para IA
Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.
Más en Liquid Glass
Dónde usarlo
Esta tarjeta glassmorphism está pensada para contenido en el que quieres que la gente se detenga: un destino en la web de una agencia de viajes, el plan destacado de una página de precios o la portada de un proyecto en tu portfolio. El efecto tilt 3D es sutil, apenas unos grados, y por eso se siente cuidado.
Usa una o dos por pantalla como mucho. Una rejilla de doce tarjetas moviéndose a la vez cansa enseguida. En el móvil no hay hover, así que la tarjeta se queda quieta y su texto tiene que funcionar solo.