Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
TSX
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>
  )
}
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-card
Dependenciesnpm install framer-motion lucide-react

Usage Example

tsx
import GlassPricingCard from "@/components/oblivion/glass-pricing-card"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <GlassPricingCard />
    </div>
  )
}