Components

GitHub Contributors

A beautiful component that displays GitHub repository contributors with rich tooltips, animations, and contribution statistics.

API
import { GitHubContributors } from "@/components/ui/github-contributors";
 
export function GitHubContributorsDemo() {
  return (
    <div className="flex flex-col gap-8">
      <div className="flex flex-col gap-3">
        <h3 className="font-medium text-sm">GitHub Contributors</h3>
        <GitHubContributors repo="vercel/next.js" limit={12} />
      </div>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://jolyui.dev/r/github-contributors"

Manual

Install the following dependencies:

npm install @radix-ui/react-tooltip lucide-react motion

Copy and paste the following code into your project.

"use client";
 
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
    Tooltip,
    TooltipContent,
    TooltipTrigger,
} from "@/components/ui/tooltip";
import { GitHubContributors as GitHubContributorsHook } from "@jolyui/github-contributors";
import { ExternalLink } from "lucide-react";
import { motion } from "motion/react";
 
interface GitHubContributorsProps {
  repo: string;
  limit?: number;
  className?: string;
  token?: string;
}
 
export function GitHubContributors({
  repo,
  limit = 12,
  className = "",
  token,
}: GitHubContributorsProps) {
  const {
    contributors,
    loading,
    error,
    totalCount,
    shown,
    remaining,
    maxContrib,
    repoUrl,
    contributorsUrl,
  } = GitHubContributorsHook({ repo, limit, token });
 
  return (
    <Card
      className={`border bg-background shadow-sm rounded-lg overflow-hidden ${className}`}
      aria-live="polite"
    >
      {/* Content */}
      <CardContent className="px-4 py-3">
        {error && (
          <p className="text-sm text-destructive mb-2">Failed: {error}</p>
        )}
 
        {loading ? (
          <div className="grid grid-cols-6 gap-3 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12">
            {Array.from({ length: limit }).map((_, i) => (
              <div key={i} className="flex items-center justify-center">
                <Skeleton className="h-10 w-10 rounded-full" />
              </div>
            ))}
          </div>
        ) : (
          <>
            <div className="grid grid-cols-6 gap-3 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-11 items-center">
              {contributors.map((c, idx) => {
                const pct = Math.round((c.contributions / maxContrib) * 100);
                const isTop = idx === 0; // highlight the top contributor in shown list
                return (
                  <div
                    key={c.id}
                    className="relative flex items-center justify-center"
                  >
                    {/* top badge */}
                    {isTop && (
                      <div className="absolute -top-1 -right-1 z-10">
                        <div className="rounded-full bg-yellow-400/90 text-[10px] font-semibold h-4 w-4 shadow-sm flex items-center justify-center border border-white text-white">
                          <span>★</span>
                        </div>
                      </div>
                    )}
 
                    <Tooltip>
                      <TooltipTrigger asChild>
                        <motion.a
                          href={c.html_url}
                          target="_blank"
                          rel="noopener noreferrer"
                          title={`${c.login} — ${c.contributions} contributions`}
                          className="relative flex items-center justify-center h-10 w-10 rounded-full overflow-hidden border bg-muted/10 hover:bg-muted transition focus:outline-none focus:ring-2 focus:ring-ring"
                          whileHover={isTop ? { scale: 1.07 } : { scale: 1.04 }}
                          whileFocus={{ scale: 1.04 }}
                          onClick={(e) => e.stopPropagation()}
                          aria-label={`${c.login} GitHub profile`}
                        >
                          <img
                            src={c.avatar_url}
                            alt={c.login}
                            className="object-cover h-full w-full"
                          />
                          {/* subtle ring on hover via pseudo element class; already handled by tailwind tokens */}
                        </motion.a>
                      </TooltipTrigger>
 
                      <TooltipContent
                        side="top"
                        align="center"
                        className="w-64 p-0 bg-transparent shadow-none"
                      >
                        <motion.div
                          initial={{ opacity: 0, scale: 0.96, y: 6 }}
                          animate={{ opacity: 1, scale: 1, y: 0 }}
                          transition={{ duration: 0.12 }}
                          className="rounded-lg border bg-popover text-popover-foreground shadow-md p-3"
                          role="dialog"
                          aria-label={`${c.login} contributor details`}
                        >
                          <div className="flex items-center gap-3">
                            <div className="h-12 w-12 rounded-md overflow-hidden border flex-shrink-0">
                              <img
                                src={c.avatar_url}
                                alt={c.login}
                                className="object-cover w-full h-full"
                              />
                            </div>
 
                            <div className="min-w-0 flex-1">
                              <div className="flex items-center gap-2">
                                <div className="font-medium text-foreground truncate">
                                  {c.login}
                                </div>
                                <div className="ml-auto text-xs text-muted-foreground font-mono">
                                  #{c.id}
                                </div>
                              </div>
 
                              <div className="mt-1 text-xs text-muted-foreground">
                                {c.contributions.toLocaleString()} contributions
                              </div>
 
                              {/* mini contribution bar */}
                              <div className="mt-2">
                                <div className="bg-muted h-2 rounded-full w-full overflow-hidden">
                                  <div
                                    className="h-2 rounded-full bg-primary transition-all duration-300"
                                    style={{ width: `${pct}%` }}
                                    aria-hidden
                                  />
                                </div>
                                <div className="text-[11px] text-muted-foreground mt-1">
                                  {pct}% of top contributor
                                </div>
                              </div>
                            </div>
                          </div>
 
                          <div className="mt-3 flex items-center justify-between gap-2">
                            <a
                              href={c.html_url}
                              target="_blank"
                              rel="noopener noreferrer"
                              className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm font-medium bg-secondary text-secondary-foreground hover:bg-secondary/80 transition-colors"
                              onClick={(e) => e.stopPropagation()}
                              aria-label={`Open ${c.login} on GitHub`}
                            >
                              <ExternalLink className="h-4 w-4" />
                              <span className="text-sm font-medium">
                                View profile
                              </span>
                            </a>
 
                            <div className="text-xs text-muted-foreground">
                              Contributions:{" "}
                              <span className="font-medium text-foreground">
                                {c.contributions}
                              </span>
                            </div>
                          </div>
                        </motion.div>
                      </TooltipContent>
                    </Tooltip>
                  </div>
                );
              })}
 
              {/* +N tile (or generic + tile) */}
              {remaining !== null && remaining > 0 ? (
                <a
                  href={contributorsUrl}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex items-center justify-center h-10 w-10 rounded-full bg-muted/5 border text-xs text-muted-foreground hover:bg-muted transition"
                  aria-label={`View all ${totalCount} contributors`}
                >
                  +{remaining}
                </a>
              ) : remaining === null &&
                !loading &&
                contributors.length === limit ? (
                <a
                  href={contributorsUrl}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex items-center justify-center h-10 w-10 rounded-full bg-muted/5 border text-xs text-muted-foreground hover:bg-muted transition"
                  aria-label={`View more contributors`}
                >
                  +
                </a>
              ) : null}
            </div>
          </>
        )}
      </CardContent>
 
      {/* Footer CTA */}
      <div className="px-4 py-3 border-t bg-background/50 flex items-center justify-between gap-3">
        <div className="min-w-0">
          <CardTitle className="text-sm font-semibold truncate m-0">
            {repo}
          </CardTitle>
          <div className="text-xs text-muted-foreground">
            {loading ? (
              <span>Loading…</span>
            ) : error ? (
              <span className="text-destructive">Failed to load</span>
            ) : (
              <span>
                Showing <span className="font-medium">{shown}</span>
                {totalCount ? (
                  <>
                    {" "}
                    of <span className="font-medium">{totalCount}</span>
                  </>
                ) : shown === limit ? (
                  <> (more)</>
                ) : null}
              </span>
            )}
          </div>
        </div>
 
        <div className="flex items-center gap-2">
          <a
            href={repoUrl}
            target="_blank"
            rel="noopener noreferrer"
            className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium hover:bg-muted transition"
            aria-label={`Open ${repo} on GitHub`}
          >
            <ExternalLink className="h-4 w-4" />
            <span>Open repo</span>
          </a>
        </div>
      </div>
    </Card>
  );
}

Usage

import { GitHubContributors } from "@/components/ui/github-contributors";

<GitHubContributors repo="vercel/next.js" limit={12} />

API Reference

GitHubContributors

The main component that fetches and displays GitHub contributors.

Prop

Type

Notes

  • The component uses GitHub's public API which has rate limits
  • Without authentication: 60 requests per hour per IP
  • With token: 5,000 requests per hour
  • Contributors are sorted by contribution count
  • The top contributor gets a special star badge
  • Tooltips show detailed contribution statistics

How is this guide?