/Components
buttons

GitHub Button

A set of premium GitHub-focused action buttons with glow lines, keyframe animations, and tooltip overlays.

buttongithubsocialglowgradienttooltipanimated

Preview

Classic Pill
Tooltip Icon
Star on GitHub

Rainbow Star Glow
Gradient Glow

Customize Glow Color

🖲Usage

example-usage.tsx
import { GithubButton } from "@/components/kanso/github-button"

export default function GithubButtonDemo() {
  return (
    <div className="flex flex-col gap-4">
      <GithubButton variantDesign="classic" href="https://github.com/Nagraj-13/Kanso-UI" />
      <GithubButton variantDesign="rainbow" href="https://github.com/Nagraj-13/Kanso-UI" />
      <GithubButton variantDesign="glow" href="https://github.com/Nagraj-13/Kanso-UI" glowColor="violet" />
    </div>
  )
}

Installation

1

Create folder & copy source

Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named github-button.tsx inside it.

2

Add global styling

Add the custom keyframe animations and styles to your global stylesheet (e.g. app/globals.css):

globals.css
/* Add to app/globals.css */
@theme inline {
  --animate-rainbow: rainbow 3s linear infinite;
  --animate-glow-translate: border-glow-translate 6s ease-in-out infinite alternate;
  --animate-glow-translate-right: border-glow-translate-right 6s ease-in-out infinite alternate;
  --animate-glow-scale: border-glow-scale 6s ease-in-out infinite alternate;
  --animate-star-shine: star-shine 6s ease-in-out infinite alternate;

  @keyframes rainbow {
    0%, 100% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
  }
  @keyframes border-glow-translate {
    0% { transform: translate(-30%, -30%); }
    100% { transform: translate(30%, 30%); }
  }
  @keyframes border-glow-translate-right {
    0% { transform: translate(30%, 30%); }
    100% { transform: translate(-30%, -30%); }
  }
  @keyframes border-glow-scale {
    0% { transform: scale(0.8) translate(-50%, -50%); }
    100% { transform: scale(1.2) translate(-50%, -50%); }
  }
  @keyframes star-shine {
    0% { transform: scale(0.9) translate(-50%, -50%); opacity: 0.2; }
    100% { transform: scale(1.3) translate(-50%, -50%); opacity: 0.5; }
  }
}

.animate-rainbow-button {
  background-image: linear-gradient(#fff, #fff),
    linear-gradient(#fff 50%, rgba(255, 255, 255, 0.6) 80%, rgba(0, 0, 0, 0)),
    linear-gradient(90deg, hsl(0, 100%, 63%), hsl(90, 100%, 63%), hsl(210, 100%, 63%), hsl(195, 100%, 63%), hsl(270, 100%, 63%)) !important;
  background-size: 100% 100%, 100% 100%, 200% 100% !important;
  background-clip: padding-box, border-box, border-box !important;
  background-origin: border-box !important;
  border: calc(0.08 * 1rem) solid transparent !important;
}

.dark .animate-rainbow-button {
  background-image: linear-gradient(#121213, #121213),
    linear-gradient(#121213 50%, rgba(18, 18, 19, 0.6) 80%, rgba(18, 18, 19, 0)),
    linear-gradient(90deg, hsl(0, 100%, 63%), hsl(90, 100%, 63%), hsl(210, 100%, 63%), hsl(195, 100%, 63%), hsl(270, 100%, 63%)) !important;
}
3

Required helper files

Ensure your project has the following helper files configured:

  • lib/utils
  • components/ui/button

<>Source Code

github-button.tsx
import * as React from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

export interface GithubButtonProps
  extends React.ComponentPropsWithoutRef<typeof Button> {
  /** The design variant of the GitHub button */
  variantDesign?: "rainbow" | "classic" | "tooltip" | "glow"
  /** Number of stars to display (static or fallback value) */
  stars?: number | string
  /** The repository link or target URL */
  href?: string
  /** Custom glow color (preset name or CSS color/gradient) for the glow variant */
  glowColor?: string
  /** Specific GitHub repository path (e.g. "owner/repo"). If omitted, parsed from href. */
  repo?: string
}

// Production-ready shared client-side cache & pending fetch registry
const starCache = new Map<string, number>()
const pendingFetches = new Map<string, Promise<number>>()

type StarListener = (count: number) => void
const starListeners = new Map<string, Set<StarListener>>()

const subscribeToRepoStars = (repo: string, callback: StarListener) => {
  if (!starListeners.has(repo)) {
    starListeners.set(repo, new Set())
  }
  starListeners.get(repo)!.add(callback)
  return () => {
    starListeners.get(repo)?.delete(callback)
    if (starListeners.get(repo)?.size === 0) {
      starListeners.delete(repo)
    }
  }
}

const broadcastRepoStars = (repo: string, count: number) => {
  starListeners.get(repo)?.forEach((cb) => cb(count))
}

const fetchRepoStars = async (repo: string): Promise<number> => {
  if (starCache.has(repo)) {
    return starCache.get(repo)!
  }
  if (pendingFetches.has(repo)) {
    return pendingFetches.get(repo)!
  }

  const promise = (async () => {
    try {
      const res = await fetch(`https://api.github.com/repos/${repo}`)
      if (!res.ok) throw new Error("GitHub API response error")
      const data = await res.json()
      const count = data.stargazers_count
      if (typeof count === "number") {
        starCache.set(repo, count)
        broadcastRepoStars(repo, count)
        return count
      }
      throw new Error("Invalid count format")
    } catch (err) {
      console.error(`Failed to fetch stars for repo ${repo}:`, err)
      return 0
    } finally {
      pendingFetches.delete(repo)
    }
  })()

  pendingFetches.set(repo, promise)
  return promise
}

const GithubButton = React.forwardRef<HTMLButtonElement, GithubButtonProps>(
  ({ className, variantDesign = "classic", stars = 11, href, glowColor, repo, children, ...props }, ref) => {
    // Shared GitHub SVG Logo path
    const githubIcon = (
      <svg className="size-4 shrink-0" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
        />
      </svg>
    )

    // Resolve glow color to a CSS background gradient
    const getGlowBg = (color?: string) => {
      if (!color) {
        return "linear-gradient(135deg, rgb(122, 105, 249), rgb(242, 99, 120), rgb(245, 131, 63))"
      }
      const presets: Record<string, string> = {
        violet: "linear-gradient(135deg, oklch(0.6 0.25 300), oklch(0.5 0.3 320))",
        rose: "linear-gradient(135deg, oklch(0.65 0.25 0), oklch(0.55 0.3 20))",
        emerald: "linear-gradient(135deg, oklch(0.7 0.25 140), oklch(0.6 0.3 160))",
        blue: "linear-gradient(135deg, oklch(0.6 0.25 250), oklch(0.5 0.3 270))",
        orange: "linear-gradient(135deg, oklch(0.7 0.25 45), oklch(0.6 0.3 65))",
        rainbow: "linear-gradient(90deg, hsl(0, 100%, 63%), hsl(90, 100%, 63%), hsl(210, 100%, 63%), hsl(195, 100%, 63%), hsl(270, 100%, 63%))"
      }
      if (color in presets) {
        return presets[color]
      }
      if (color.includes("gradient") || color.includes("var(") || color.includes("rgb") || color.includes("hsl") || color.includes("#")) {
        return color
      }
      return `linear-gradient(135deg, ${color}, ${color}cc)`
    }

    // Dynamic Live Star Count State
    const [starCount, setStarCount] = React.useState<number | string>(stars)

    React.useEffect(() => {
      setStarCount(stars)
    }, [stars])

    React.useEffect(() => {
      let targetRepo = repo
      if (!targetRepo && href) {
        // Try parsing from GitHub URL format: github.com/owner/repo
        const match = href.match(/github\.com\/([^\/]+\/[^\/#?]+)/)
        if (match) {
          targetRepo = match[1]
        }
      }

      if (!targetRepo) return

      if (starCache.has(targetRepo)) {
        setStarCount(starCache.get(targetRepo)!)
      }

      const unsubscribe = subscribeToRepoStars(targetRepo, (count) => {
        setStarCount(count)
      })

      fetchRepoStars(targetRepo).then((count) => {
        if (count > 0) {
          setStarCount(count)
        }
      })

      return () => unsubscribe()
    }, [repo, href])

    // Standard button element to render (using primitive Button from shadcn)
    const buttonElement = (
      <Button
        ref={ref}
        variant="ghost"
        className={cn(
          "transition-all duration-200 cursor-pointer h-auto hover:bg-transparent text-inherit select-none",
          
          // Rainbow variant styling
          variantDesign === "rainbow" && [
            "animate-rainbow-button items-center justify-center whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group relative animate-rainbow text-foreground hover:scale-105 active:scale-95 px-4 py-2 inline-flex",
            "before:absolute before:bottom-[-20%] before:left-1/2 before:z-[0] before:h-[20%] before:w-[60%] before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(0,100%,63%),hsl(90,100%,63%),hsl(210,100%,63%),hsl(195,100%,63%),hsl(270,100%,63%))] before:bg-[length:200%_auto] before:[filter:blur(calc(0.8*1rem))]",
          ],

          // Classic variant styling
          variantDesign === "classic" && [
            "gap-2 rounded-full font-bold justify-center px-4 py-2.5 text-xs bg-zinc-900/90 text-white shadow-[inset_0_1px_0_0_rgba(255,255,255,0.04),inset_0_0_0_1px_rgba(255,255,255,0.04)] dark:bg-zinc-950/90 hover:bg-zinc-950 hover:text-yellow-300 dark:hover:bg-zinc-900 hover:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.08),inset_0_0_0_1px_rgba(252,232,3,0.08)] hover:-translate-y-1 hover:scale-102 transition-all duration-300 ease-out",
          ],

          // Tooltip variant styling (button portion)
          variantDesign === "tooltip" && [
            "rounded-lg p-2 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-900 dark:hover:bg-zinc-800 text-zinc-950 dark:text-zinc-50 hover:scale-110 active:scale-95 transition-all duration-200",
          ],

          // Glow variant styling
          variantDesign === "glow" && [
            "group/glow relative rounded-full p-[1.5px] overflow-hidden dark:bg-neutral-800 bg-neutral-200 border-0 hover:scale-105 active:scale-95 transition-all duration-300 hover:shadow-lg dark:hover:shadow-black/50",
          ],
          
          className
        )}
        {...props}
      >
        {variantDesign === "rainbow" && (
          <>
            <div className="flex items-center gap-1.5 relative z-10">
              {githubIcon}
              <span className="text-zinc-900 dark:text-zinc-50 font-medium">
                {children || "Star on GitHub"}
              </span>
            </div>
            <div className="ml-3 flex items-center gap-1.5 text-xs relative z-10 border-l border-zinc-200 dark:border-zinc-800 pl-3">
              <svg
                className="size-3.5 text-zinc-400 group-hover:text-yellow-500 group-hover:scale-110 transition-all duration-200"
                fill="currentColor"
                viewBox="0 0 24 24"
              >
                <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
              </svg>
              <span className="tabular-nums font-semibold text-zinc-600 dark:text-zinc-400">
                {starCount}
              </span>
            </div>
          </>
        )}

        {variantDesign === "classic" && (
          <>
            {githubIcon}
            <span>{children || "View on Github"}</span>
          </>
        )}

        {variantDesign === "tooltip" && (
          <div className="size-5 flex items-center justify-center">
            {githubIcon}
          </div>
        )}

        {variantDesign === "glow" && (
          <>
            {/* Animated Left Glow Border background */}
            <span className="absolute inset-0 rounded-full overflow-hidden">
              <span className="inset-0 absolute pointer-events-none select-none">
                <span
                  className="block -translate-x-1/2 -translate-y-1/3 size-24 blur-xl animate-glow-translate"
                  style={{
                    background: getGlowBg(glowColor),
                  }}
                />
              </span>
            </span>

            {/* Animated Right Glow Border background */}
            <span className="absolute inset-0 rounded-full overflow-hidden">
              <span className="inset-0 absolute pointer-events-none select-none">
                <span
                  className="block translate-x-1/2 translate-y-1/3 size-24 blur-xl animate-glow-translate-right"
                  style={{
                    background: getGlowBg(glowColor),
                  }}
                />
              </span>
            </span>

            {/* Left scaling glow */}
            <span className="inset-0 absolute pointer-events-none select-none animate-glow-translate">
              <span
                className="block z-0 h-full w-12 blur-xl -translate-x-1/2 rounded-full animate-glow-scale"
                style={{
                  background: getGlowBg(glowColor),
                }}
              />
            </span>

            {/* Right scaling glow */}
            <span className="inset-0 absolute pointer-events-none select-none animate-glow-translate">
              <span
                className="block z-0 h-full w-12 blur-xl translate-x-1/2 rounded-full animate-glow-scale"
                style={{
                  background: getGlowBg(glowColor),
                }}
              />
            </span>

            {/* Inner Content pill */}
            <span className="flex items-center justify-center gap-2 relative z-10 dark:bg-neutral-950/90 bg-neutral-50/90 rounded-full py-2 px-4 pl-3.5 w-full h-full text-zinc-900 dark:text-zinc-50 hover:bg-neutral-100/90 dark:hover:bg-neutral-900/90">
              <span className="relative group-hover/glow:scale-105 group-hover/glow:rotate-[360deg] transition-all duration-700 ease-in-out">
                {githubIcon}
                <span
                  className="rounded-full size-11 absolute opacity-0 dark:opacity-35 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 blur-lg animate-star-shine"
                  style={{
                    background: getGlowBg(glowColor),
                  }}
                />
              </span>
              <span className="bg-gradient-to-b ml-0.5 dark:from-white dark:to-white/50 from-neutral-950 to-neutral-950/60 bg-clip-text text-xs text-transparent font-semibold">
                {children || "Star on Github"}
              </span>
              <span className="ml-1.5 tabular-nums text-[10px] font-semibold bg-neutral-200 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 px-1.5 py-0.5 rounded-full">
                {starCount}
              </span>
            </span>
          </>
        )}
      </Button>
    )

    // Wrap in tooltip layout if variant is tooltip
    if (variantDesign === "tooltip") {
      return (
        <div className="group relative inline-block">
          {href ? (
            <a href={href} target="_blank" rel="noopener noreferrer" className="block focus:outline-none">
              {buttonElement}
            </a>
          ) : (
            buttonElement
          )}
          {/* Tooltip box */}
          <span className="absolute -top-12 left-[50%] -translate-x-[50%] z-20 origin-bottom scale-0 px-2.5 py-1.5 rounded-lg border border-zinc-200 bg-white text-xs font-semibold text-zinc-800 shadow-sm transition-all duration-200 ease-in-out group-hover:scale-100 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-200">
            {children || "GitHub"}
          </span>
        </div>
      )
    }

    if (href) {
      return (
        <a href={href} target="_blank" rel="noopener noreferrer" className="inline-block focus:outline-none">
          {buttonElement}
        </a>
      )
    }

    return buttonElement
  }
)

GithubButton.displayName = "GithubButton"

export { GithubButton }

Props

PropTypeDefaultDescription
variantDesign"classic" | "rainbow" | "tooltip" | "glow""classic"The visual style variation of the GitHub button.
starsnumber | string11The repository star count displayed on the button.
hrefstringThe target repository or profile URL.
childrenReact.ReactNodeThe label text displayed inside the button (e.g. Star on GitHub).
glowColorstringCustom glow color (preset name or CSS color/gradient) for the glow variant.