Docs
Go back
Component by
OV
OblivionTSX
glass-pricing-card
"use client"
import { useTheme } from "next-themes"
import { motion } from "framer-motion"
import { useState, type CSSProperties, useEffect } from "react"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
interface Feature {
text: string
included: boolean
}
interface GlassPricingCardProps {
planName?: string
price?: string
period?: string
description?: string
features?: Feature[]
ctaText?: string
ctaLink?: string
backgroundColor?: string
glassOpacity?: number
borderColor?: string
textColor?: string
accentColor?: string
priceColor?: string
checkmarkColor?: string
showBadge?: boolean
badgeText?: string
badgeColor?: string
className?: string
style?: CSSProperties
}
export const componentMeta = {
name: "Glass Pricing Card",
description: "A premium dark pricing card with glass surface, border on hover, and shimmer animation.",
props: {
planName: { type: "string", description: "Name of the pricing plan", default: '"Professional"' },
price: { type: "string", description: "Price value", default: '"$49"' },
period: { type: "string", description: "Pricing period", default: '"/month"' },
description: { type: "string", description: "Plan description", default: '"Perfect for growing teams"' },
features: { type: "array", description: "List of features", default: "[]" },
ctaText: { type: "string", description: "Call to action text", default: '"Get Started"' },
ctaLink: { type: "string", description: "Call to action link", default: '"#"' },
showBadge: { type: "boolean", description: "Show a badge on the top right", default: "true" },
badgeText: { type: "string", description: "Badge text", default: '"Popular"' },
accentColor: { type: "string", description: "Primary accent color", default: '"#0099FF"' },
}
}
export default function GlassPricingCard({
planName = "Professional",
price = "$49",
period = "/month",
description = "Perfect for growing teams",
features = [
{ text: "Unlimited projects", included: true },
{ text: "Advanced analytics", included: true },
{ text: "Priority support", included: true },
{ text: "Custom integrations", included: true },
{ text: "Dedicated account manager", included: false },
],
ctaText = "Get Started",
ctaLink = "#",
backgroundColor,
glassOpacity,
borderColor,
textColor,
accentColor = "#0099FF",
priceColor,
checkmarkColor = "#22CC66",
showBadge = true,
badgeText = "Popular",
badgeColor = "#FFBB00",
className,
style
}: GlassPricingCardProps) {
const { theme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const [isHovered, setIsHovered] = useState(false)
useEffect(() => setMounted(true), [])
const currentTheme = resolvedTheme || theme || "dark"
const isLight = currentTheme === "light"
// Dynamic theme-aware defaults to prevent "muddy grey" cards in light mode
const finalBackgroundColor = backgroundColor || (isLight ? "#FFFFFF" : "#030303")
const finalGlassOpacity = glassOpacity || (isLight ? 0.95 : 0.82)
const finalTextColor = textColor || (isLight ? "#18181b" : "#FFFFFF")
const finalBorderColor = borderColor || (isLight ? "#1a1a1a" : "#FFFFFF")
const finalPriceColor = priceColor || finalTextColor
// Helper to convert hex to rgba
const getRGBA = (hex: string, alpha: number) => {
if (!hex.startsWith("#")) return hex
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// Prevent hydration mismatch
if (!mounted) return <div className={cn("w-full max-w-[320px] aspect-[1/1.5] rounded-2xl bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800", className)} />
return (
<motion.div
style={{
width: "100%",
maxWidth: 320,
position: "relative",
overflow: "hidden",
borderRadius: 20,
background: getRGBA(finalBackgroundColor, finalGlassOpacity),
backdropFilter: "blur(24px)",
WebkitBackdropFilter: "blur(24px)",
border: `1px solid ${isHovered ? finalBorderColor : isLight ? "rgba(0,0,0,0.1)" : "rgba(255, 255, 255, 0.15)"}`,
padding: "24px 28px",
display: "flex",
flexDirection: "column",
gap: 20,
boxShadow: isLight
? "0 20px 48px rgba(0, 0, 0, 0.08)"
: "0 20px 48px rgba(0, 0, 0, 0.4)",
...style,
}}
className={cn("font-sans antialiased", className)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
whileHover={{ y: -4 }}
transition={{ duration: 0.3 }}
>
{showBadge && (
<div
style={{
position: "absolute",
top: 12,
right: 12,
background: badgeColor,
color: "#000",
padding: "3px 10px",
borderRadius: 20,
fontSize: 9,
fontWeight: 800,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
{badgeText}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<h3
style={{
margin: 0,
color: textColor,
fontSize: 18,
fontWeight: 600,
letterSpacing: "-0.01em",
}}
>
{planName}
</h3>
<p
style={{
margin: 0,
color: `${textColor}99`,
fontSize: 13,
fontWeight: 400,
lineHeight: 1.4,
}}
>
{description}
</p>
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
<span
style={{
color: priceColor,
fontSize: 32,
fontWeight: 700,
letterSpacing: "-0.04em",
}}
>
{price}
</span>
<span
style={{
color: `${textColor}99`,
fontSize: 14,
fontWeight: 500,
}}
>
{period}
</span>
</div>
<div
style={{
width: "100%",
height: 1,
background: `linear-gradient(90deg, transparent, ${borderColor}33, transparent)`,
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
flex: 1,
}}
>
{features.map((feature, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
gap: 10,
opacity: feature.included ? 1 : 0.4,
}}
>
<div
style={{
width: 18,
height: 18,
borderRadius: "50%",
background: feature.included
? checkmarkColor
: "transparent",
border: feature.included
? "none"
: `1.5px solid ${textColor}33`,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{feature.included && (
<Check size={10} strokeWidth={3} color="#000" />
)}
</div>
<span
style={{
color: textColor,
fontSize: 13,
fontWeight: 500,
}}
>
{feature.text}
</span>
</div>
))}
</div>
<motion.a
href={ctaLink}
style={{
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "12px 24px",
background: accentColor,
color: "#000",
borderRadius: 12,
textDecoration: "none",
overflow: "hidden",
cursor: "pointer",
fontSize: 14,
fontWeight: 600,
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
style={{
position: "absolute",
top: 0,
left: "-100%",
width: "100%",
height: "100%",
background:
"linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent)",
}}
animate={{
left: ["-100%", "200%"],
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "linear",
}}
/>
<span style={{ position: "relative", zIndex: 1 }}>
{ctaText}
</span>
</motion.a>
{/* Decorative Gradient Glow */}
<motion.div
style={{
position: "absolute",
top: -80,
right: -80,
width: 160,
height: 160,
borderRadius: "50%",
background: `radial-gradient(circle, ${accentColor}33, transparent)`,
filter: "blur(32px)",
pointerEvents: "none",
}}
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</motion.div>
)
}"use client"
import { useTheme } from "next-themes"
import { motion } from "framer-motion"
import { useState, type CSSProperties, useEffect } from "react"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
interface Feature {
text: string
included: boolean
}
interface GlassPricingCardProps {
planName?: string
price?: string
period?: string
description?: string
features?: Feature[]
ctaText?: string
ctaLink?: string
backgroundColor?: string
glassOpacity?: number
borderColor?: string
textColor?: string
accentColor?: string
priceColor?: string
checkmarkColor?: string
showBadge?: boolean
badgeText?: string
badgeColor?: string
className?: string
style?: CSSProperties
}
export const componentMeta = {
name: "Glass Pricing Card",
description: "A premium dark pricing card with glass surface, border on hover, and shimmer animation.",
props: {
planName: { type: "string", description: "Name of the pricing plan", default: '"Professional"' },
price: { type: "string", description: "Price value", default: '"$49"' },
period: { type: "string", description: "Pricing period", default: '"/month"' },
description: { type: "string", description: "Plan description", default: '"Perfect for growing teams"' },
features: { type: "array", description: "List of features", default: "[]" },
ctaText: { type: "string", description: "Call to action text", default: '"Get Started"' },
ctaLink: { type: "string", description: "Call to action link", default: '"#"' },
showBadge: { type: "boolean", description: "Show a badge on the top right", default: "true" },
badgeText: { type: "string", description: "Badge text", default: '"Popular"' },
accentColor: { type: "string", description: "Primary accent color", default: '"#0099FF"' },
}
}
export default function GlassPricingCard({
planName = "Professional",
price = "$49",
period = "/month",
description = "Perfect for growing teams",
features = [
{ text: "Unlimited projects", included: true },
{ text: "Advanced analytics", included: true },
{ text: "Priority support", included: true },
{ text: "Custom integrations", included: true },
{ text: "Dedicated account manager", included: false },
],
ctaText = "Get Started",
ctaLink = "#",
backgroundColor,
glassOpacity,
borderColor,
textColor,
accentColor = "#0099FF",
priceColor,
checkmarkColor = "#22CC66",
showBadge = true,
badgeText = "Popular",
badgeColor = "#FFBB00",
className,
style
}: GlassPricingCardProps) {
const { theme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const [isHovered, setIsHovered] = useState(false)
useEffect(() => setMounted(true), [])
const currentTheme = resolvedTheme || theme || "dark"
const isLight = currentTheme === "light"
// Dynamic theme-aware defaults to prevent "muddy grey" cards in light mode
const finalBackgroundColor = backgroundColor || (isLight ? "#FFFFFF" : "#030303")
const finalGlassOpacity = glassOpacity || (isLight ? 0.95 : 0.82)
const finalTextColor = textColor || (isLight ? "#18181b" : "#FFFFFF")
const finalBorderColor = borderColor || (isLight ? "#1a1a1a" : "#FFFFFF")
const finalPriceColor = priceColor || finalTextColor
// Helper to convert hex to rgba
const getRGBA = (hex: string, alpha: number) => {
if (!hex.startsWith("#")) return hex
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// Prevent hydration mismatch
if (!mounted) return <div className={cn("w-full max-w-[320px] aspect-[1/1.5] rounded-2xl bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800", className)} />
return (
<motion.div
style={{
width: "100%",
maxWidth: 320,
position: "relative",
overflow: "hidden",
borderRadius: 20,
background: getRGBA(finalBackgroundColor, finalGlassOpacity),
backdropFilter: "blur(24px)",
WebkitBackdropFilter: "blur(24px)",
border: `1px solid ${isHovered ? finalBorderColor : isLight ? "rgba(0,0,0,0.1)" : "rgba(255, 255, 255, 0.15)"}`,
padding: "24px 28px",
display: "flex",
flexDirection: "column",
gap: 20,
boxShadow: isLight
? "0 20px 48px rgba(0, 0, 0, 0.08)"
: "0 20px 48px rgba(0, 0, 0, 0.4)",
...style,
}}
className={cn("font-sans antialiased", className)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
whileHover={{ y: -4 }}
transition={{ duration: 0.3 }}
>
{showBadge && (
<div
style={{
position: "absolute",
top: 12,
right: 12,
background: badgeColor,
color: "#000",
padding: "3px 10px",
borderRadius: 20,
fontSize: 9,
fontWeight: 800,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
{badgeText}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<h3
style={{
margin: 0,
color: textColor,
fontSize: 18,
fontWeight: 600,
letterSpacing: "-0.01em",
}}
>
{planName}
</h3>
<p
style={{
margin: 0,
color: `${textColor}99`,
fontSize: 13,
fontWeight: 400,
lineHeight: 1.4,
}}
>
{description}
</p>
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
<span
style={{
color: priceColor,
fontSize: 32,
fontWeight: 700,
letterSpacing: "-0.04em",
}}
>
{price}
</span>
<span
style={{
color: `${textColor}99`,
fontSize: 14,
fontWeight: 500,
}}
>
{period}
</span>
</div>
<div
style={{
width: "100%",
height: 1,
background: `linear-gradient(90deg, transparent, ${borderColor}33, transparent)`,
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
flex: 1,
}}
>
{features.map((feature, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
gap: 10,
opacity: feature.included ? 1 : 0.4,
}}
>
<div
style={{
width: 18,
height: 18,
borderRadius: "50%",
background: feature.included
? checkmarkColor
: "transparent",
border: feature.included
? "none"
: `1.5px solid ${textColor}33`,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{feature.included && (
<Check size={10} strokeWidth={3} color="#000" />
)}
</div>
<span
style={{
color: textColor,
fontSize: 13,
fontWeight: 500,
}}
>
{feature.text}
</span>
</div>
))}
</div>
<motion.a
href={ctaLink}
style={{
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "12px 24px",
background: accentColor,
color: "#000",
borderRadius: 12,
textDecoration: "none",
overflow: "hidden",
cursor: "pointer",
fontSize: 14,
fontWeight: 600,
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
style={{
position: "absolute",
top: 0,
left: "-100%",
width: "100%",
height: "100%",
background:
"linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent)",
}}
animate={{
left: ["-100%", "200%"],
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "linear",
}}
/>
<span style={{ position: "relative", zIndex: 1 }}>
{ctaText}
</span>
</motion.a>
{/* Decorative Gradient Glow */}
<motion.div
style={{
position: "absolute",
top: -80,
right: -80,
width: 160,
height: 160,
borderRadius: "50%",
background: `radial-gradient(circle, ${accentColor}33, transparent)`,
filter: "blur(32px)",
pointerEvents: "none",
}}
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</motion.div>
)
}Save to favoritesCopy to Figma
React
Glass Pricing Card
A premium dark pricing card with glass surface, border on hover, and shimmer animation.
Installation
$ npx @cosmoo/oblivion add glass-pricing-cardDependencies
npm install framer-motion lucide-reactUsage Example
tsx
import GlassPricingCard from "@/components/oblivion/glass-pricing-card"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassPricingCard />
</div>
)
}import GlassPricingCard from "@/components/oblivion/glass-pricing-card"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassPricingCard />
</div>
)
}