buttons
GitHub Button
A set of premium GitHub-focused action buttons with glow lines, keyframe animations, and tooltip overlays.
buttongithubsocialglowgradienttooltipanimated
▶Preview
🖲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 };