Skip to content
Docs
GitHub Card - Shiny

GitHub Card - Shiny

A card component where a shiny background follows the mouse when hovered, as seen in GitHub's homepage

requires interactionhover
Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/card/github-card-shiny.json

Manual

Copy useMousePosition hook

import { useEffect, useRef } from "react";
 
type Point = {
  x: number;
  y: number;
};
 
export function useMousePosition(
  ref: React.RefObject<HTMLElement | null>,
  callback?: (point: Point) => void,
) {
  const callbackRef = useRef(callback);
  const rectRef = useRef<DOMRect | null>(null);
  const frameRef = useRef<number | null>(null);
  const latestPointRef = useRef<Point | null>(null);
 
  callbackRef.current = callback;
 
  useEffect(() => {
    const node = ref.current;
    if (!node) return;
 
    const updateRect = () => {
      rectRef.current = node.getBoundingClientRect();
    };
 
    const flush = () => {
      frameRef.current = null;
 
      if (latestPointRef.current) {
        callbackRef.current?.(latestPointRef.current);
      }
    };
 
    const handlePointerMove = (event: PointerEvent) => {
      if (!rectRef.current) {
        updateRect();
      }
 
      const currentRect = rectRef.current;
      if (!currentRect) return;
 
      latestPointRef.current = {
        x: event.clientX - currentRect.left,
        y: event.clientY - currentRect.top,
      };
 
      if (frameRef.current === null) {
        frameRef.current = requestAnimationFrame(flush);
      }
    };
 
    updateRect();
 
    node.addEventListener("pointerenter", updateRect);
    node.addEventListener("pointermove", handlePointerMove, {
      passive: true,
    });
 
    window.addEventListener("resize", updateRect);
    window.addEventListener("scroll", updateRect, {
      passive: true,
      capture: true,
    });
 
    return () => {
      node.removeEventListener("pointerenter", updateRect);
      node.removeEventListener("pointermove", handlePointerMove);
 
      window.removeEventListener("resize", updateRect);
      window.removeEventListener("scroll", updateRect, true);
 
      if (frameRef.current !== null) {
        cancelAnimationFrame(frameRef.current);
      }
    };
  }, [ref]);
}

Run the following command

It will create a new file github-card-shiny.tsx inside the components/animata/card directory.

mkdir -p components/animata/card && touch components/animata/card/github-card-shiny.tsx

Paste the code

Open the newly created file and paste the following code:

import { CheckCircle2 } from "lucide-react";
import { useCallback, useRef } from "react";
 
import { useMousePosition } from "@/hooks/use-mouse-position";
import { cn } from "@/lib/utils";
 
export default function GithubCardShiny({ className }: { className?: string }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const overlayRef = useRef<HTMLDivElement>(null);
 
  const update = useCallback(({ x, y }: { x: number; y: number }) => {
    if (!overlayRef.current) {
      return;
    }
 
    const { width, height } = overlayRef.current?.getBoundingClientRect() ?? {};
    const xOffset = x - width / 2;
    const yOffset = y - height / 2;
 
    overlayRef.current?.style.setProperty("--x", `${xOffset}px`);
    overlayRef.current?.style.setProperty("--y", `${yOffset}px`);
  }, []);
 
  useMousePosition(containerRef, update);
 
  return (
    <div
      ref={containerRef}
      className={cn(
        "group/shiny relative w-96 min-w-fit max-w-full overflow-hidden rounded-md border border-border bg-zinc-700 p-6 text-zinc-200 shadow-lg",
        className,
      )}
    >
      <div
        ref={overlayRef}
        // Adjust height & width as required
        className="z-0 absolute h-64 w-64 rounded-full bg-white opacity-0 bg-blend-soft-light blur-3xl transition-opacity group-hover/shiny:opacity-20"
        style={{
          transform: "translate(var(--x), var(--y))",
        }}
      />
 
      <div className="font-mono text-sm">
        cmake.yml
        <div className="text-xs text-zinc-400">on: push</div>
      </div>
 
      <div className="z-10 mt-10 flex w-full min-w-fit flex-col gap-2 rounded-md bg-zinc-600 p-4 shadow-2xl">
        {[
          {
            title: "Install dependencies",
            time: "1m 20s",
          },
          {
            title: "Build",
            time: "2m 10s",
          },
          {
            title: "Test",
            time: "1m 30s",
          },
        ].map((step) => {
          return (
            <div className="flex w-full items-center gap-2" key={step.title}>
              <CheckCircle2 className="shrink-0 fill-green-400 text-zinc-600" />
              <strong className="text-xs md:shrink-0 md:text-base">{step.title}</strong>
 
              <span className="ml-auto inline-block shrink-0 text-xs opacity-75">{step.time}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Credits

Built by hari

Inspired by GitHub's homepage