Glowing Border Card

PreviousNext

A customizable card with a glowing border effect that changes colors based on light and dark modes.

Docs
scrollxuicomponent

Preview

Loading preview…
components/ui/heroui.tsx
"use client";
import { useEffect, useState, useRef } from "react";
import { motion, useTransform, useScroll } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import LustreText from "@/components/ui/lustretext";
import { useTheme } from "next-themes";
import Globe from "@/components/ui/globe";
import { cn } from "@/lib/utils";

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.2,
      delayChildren: 0.4,
      duration: 0.8,
    },
  },
};

const textVariants = {
  hidden: { y: 40, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { type: "spring", stiffness: 120, damping: 15 },
  },
};

declare module "@/components/ui/badge" {
  interface BadgeProps {
    shiny?: boolean;
    shinySpeed?: number;
  }
}

interface HeroUIProps {
  title?: string;
  subtitle?: string;
  badgeText?: string;
  primaryCTA?: string;
  secondaryCTA?: string;
  features?: string[];
  className?: string;
  globeBaseColor?: {
    light: [number, number, number];
    dark: [number, number, number];
  };
  globeMarkerColor?: {
    light: [number, number, number];
    dark: [number, number, number];
  };
  globeGlowColor?: {
    light: [number, number, number];
    dark: [number, number, number];
  };
}

export default function HeroUI({
  title = "ScrollX UI",
  subtitle = "Where Interactions Spark Joy",
  badgeText = "✨ Now Open Source",
  primaryCTA = "Get Started",
  secondaryCTA = "Documentation",
  features = [
    "TypeScript First",
    "Dark Mode",
    "100% Customizable",
    "MIT Licensed",
  ],
  className = "",
  globeBaseColor = {
    light: [0.98, 0.98, 0.98],
    dark: [0.12, 0.12, 0.12],
  },
  globeMarkerColor = {
    light: [0.2, 0.5, 0.9],
    dark: [0.1, 0.8, 1],
  },
  globeGlowColor = {
    light: [0.3, 0.3, 0.3],
    dark: [1, 1, 1],
  },
}: HeroUIProps) {
  const { theme } = useTheme();
  const [mounted, setMounted] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start start", "end start"],
  });
  const y = useTransform(scrollYProgress, [0, 1], ["0%", "30%"]);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <section
      ref={ref}
      className={cn(
        "relative overflow-hidden min-h-[90vh] flex items-center",
        className
      )}
    >
      <div className="absolute inset-0 z-0">
        <div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#374151_1px,transparent_1px)] [background-size:16px_16px]" />
        <motion.div
          className="absolute top-20 left-1/4 w-60 h-60 sm:w-80 sm:h-80 md:w-96 md:h-96 lg:w-[30rem] lg:h-[30rem] bg-gradient-to-r from-blue-500/20 to-cyan-500/20 rounded-full blur-[100px]"
          animate={{
            scale: [1, 1.2, 1],
            rotate: [0, 180, 360],
          }}
          transition={{
            duration: 20,
            repeat: Infinity,
            ease: "linear",
          }}
        />
      </div>

      <div className="container relative z-10 mx-auto px-4 py-6 sm:py-8 md:py-16 lg:py-24">
        <div className="relative min-h-[60vh] md:min-h-[70vh]">
          <motion.div
            className="absolute right-0 bottom-4 top-auto w-[70%] md:top-2 md:bottom-auto md:w-1/2 lg:top-[-60px] lg:right-[-40px] xl:top-[-80px] xl:right-[-60px] transition-all duration-500"
            style={{ y }}
            initial={{ scale: 0.8, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            transition={{ type: "spring", stiffness: 100, damping: 15 }}
          >
            <div className="w-full h-[220px] md:h-[300px] lg:h-[400px] xl:h-[450px]">
              <Globe
                baseColor={
                  theme === "dark"
                    ? (globeBaseColor.dark as [number, number, number])
                    : (globeBaseColor.light as [number, number, number])
                }
                markerColor={
                  theme === "dark"
                    ? (globeMarkerColor.dark as [number, number, number])
                    : (globeMarkerColor.light as [number, number, number])
                }
                glowColor={
                  theme === "dark"
                    ? (globeGlowColor.dark as [number, number, number])
                    : (globeGlowColor.light as [number, number, number])
                }
              />
            </div>
          </motion.div>

          <motion.div
            className="relative z-20 w-full md:w-7/12 lg:w-1/2 pt-6 sm:pt-8 md:pt-16 lg:pt-24 md:ml-8 lg:ml-16 md:-mt-6"
            variants={containerVariants}
            initial="hidden"
            animate="visible"
          >
            <Badge
              variant="destructive"
              className="w-fit mb-4"
              shiny
              shinySpeed={3}
            >
              {badgeText}
            </Badge>

            <motion.h1
              className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight"
              variants={textVariants}
            >
              <span className="block bg-gradient-to-r from-blue-600 via-cyan-500 to-blue-400 bg-clip-text text-transparent">
                {title}
              </span>
              <span className="text-foreground mt-2 block">
                {subtitle.includes("Interactions") ? (
                  <>
                    Where <LustreText text="Interactions" speed={8} className="inline" /> <span className="border-b-4 border-cyan-400/50">Spark Joy</span>
                  </>
                ) : (
                  subtitle
                )}
              </span>
            </motion.h1>

            <motion.div
              className="text-lg sm:text-xl md:text-2xl text-muted-foreground max-w-2xl mt-4"
              variants={textVariants}
            >
              Animated, accessible, and customizable components built with
              <Badge variant="default" className="mx-1">Framer Motion</Badge> and
              <Badge variant="default" className="mx-1">Tailwind CSS</Badge>
            </motion.div>

            <motion.div
              className="flex gap-4 mt-6 max-[375px]:flex-col max-[375px]:w-full max-[375px]:gap-3"
              variants={textVariants}
            >
              <Button
                size="lg"
                className="group relative overflow-hidden rounded-xl px-8 py-6 text-lg font-semibold shadow-xl max-[375px]:w-full max-[375px]:px-4 max-[375px]:py-4"
              >
                <LustreText
                  text={primaryCTA}
                  speed={6}
                  className="relative z-10 bg-white dark:bg-red-950 text-[clamp(0.75rem,2vw,1.25rem)]"
                />
                <div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-cyan-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
              </Button>

              <Button
                variant="outline"
                size="lg"
                className="rounded-xl px-8 py-6 text-lg font-semibold border-2 backdrop-blur-sm max-[375px]:w-full max-[375px]:px-4 max-[375px]:py-4"
              >
                <span className="bg-gradient-to-r from-foreground to-muted-foreground bg-clip-text text-transparent">
                  {secondaryCTA}
                </span>
              </Button>
            </motion.div>

            <motion.div
              className="flex items-center gap-8 mt-12 opacity-70"
              variants={textVariants}
            >
              <div className="flex gap-4 flex-wrap">
                {features.map((text) => (
                  <Badge
                    key={text}
                    variant="outline"
                    className="py-1.5 px-3 text-sm border-muted-foreground/30 bg-background/50"
                  >
                    {text}
                  </Badge>
                ))}
              </div>
            </motion.div>
          </motion.div>
        </div>
      </div>
    </section>
  );
}

Installation

npx shadcn@latest add @scrollxui/heroui

Usage

import { Heroui } from "@/components/heroui"
<Heroui />