rimubhai_
PortfolioBlogComponents

© 2026 rimubhai_ All rights reserved.

ContactPrivacy

GitHub Activity Graph

A beautiful, interactive component to display GitHub contribution activity with real-time fetching, smart formatting, zero layout shift, and zero API keys required.

⚡ Zero API Key Required📅 365 Days Heatmap🎨 Light & Dark ThemesResponsive Scroll Layout

Interactive Studio

Enter a GitHub username to preview their contribution chart.

Quick try:
Loading heatmap...

Zero API Key Needed

Fetches public contribution SVGs directly without personal access tokens or OAuth setups.

365-Day Activity

Renders a full year of daily contribution intensity blocks with interactive tooltips.

Theme Adaptive

Matches light & dark themes automatically using next-themes.

Zero Layout Shift

Includes Skeleton loading states and fallback error states for seamless rendering.

Manual Installation Guide

01

Install Dependencies

Install react-activity-calendar, date-fns, and axios.

pnpm add react-activity-calendar date-fns axios lucide-react
02

Create Component File

Create components/ui/github-heatmap.tsx.

"use client"

import { useEffect, useState } from "react"
import { ActivityCalendar, type Activity, type ThemeInput } from "react-activity-calendar"
import { ExternalLink, AlertCircle } from "lucide-react"
import Link from "next/link"
import { useTheme } from "next-themes"
import { format } from "date-fns"
import * as React from "react"
import axios from "axios"

import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"

export interface GithubContributionsProps {
  username?: string
}

export interface ContributionDay {
  date: string
  count: number
  level: 0 | 1 | 2 | 3 | 4
}

export interface ApiResponse {
  total: {
    [year: number]: number
    [year: string]: number
  }
  contributions: ContributionDay[]
}

export function GithubHeatMap({ username }: GithubContributionsProps) {
  const { theme, systemTheme } = useTheme()

  const targetUsername = username || process.env.NEXT_PUBLIC_GITHUB_USERNAME || "rimu-7"

  const [data, setData] = useState<ContributionDay[]>([])
  const [total, setTotal] = useState(0)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [prevUsername, setPrevUsername] = useState<string>(targetUsername)

  if (targetUsername !== prevUsername) {
    setPrevUsername(targetUsername)
    setIsLoading(true)
    setError(null)
  }

  useEffect(() => {
    let isMounted = true
    const controller = new AbortController()

    const loadData = async () => {
      if (!targetUsername) return

      try {
        const response = await axios.get<ApiResponse>(
          `https://github-contributions-api.jogruber.de/v4/${targetUsername}?y=last`,
          { signal: controller.signal }
        )

        if (!isMounted) return

        const responseData = response.data

        if (!responseData?.contributions) {
          setData([])
          setTotal(0)
          return
        }

        setData(responseData.contributions)
        setTotal(responseData.contributions.reduce((sum, day) => sum + day.count, 0))
      } catch (err: any) {
        if (!isMounted || axios.isCancel(err)) return

        console.error("Github API Error:", err)
        if (err.response?.status === 404) {
          setError("GitHub user not found")
        } else {
          setError(err.message || "An API error occurred")
        }
      } finally {
        if (isMounted) setIsLoading(false)
      }
    }

    loadData()

    return () => {
      isMounted = false
      controller.abort()
    }
  }, [targetUsername])

  const currentTheme = theme === "system" ? systemTheme : theme
  const colorTheme: ThemeInput = {
    light: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
    dark: ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
  }

  const renderBlock = (block: React.ReactElement, activity: Activity) => {
    const triggerItem = React.cloneElement(block as React.ReactElement<any>, {
      className: "cursor-pointer hover:opacity-80 transition-opacity",
    })

    return (
      <Tooltip key={activity.date}>
        <TooltipTrigger asChild>{triggerItem}</TooltipTrigger>
        <TooltipContent side="top" className="bg-popover text-popover-foreground shadow-md border">
          <div className="text-xs text-center">
            <div className="font-bold">{activity.count === 0 ? "No" : activity.count} contributions</div>
            <div className="text-muted-foreground">{format(new Date(activity.date), "MMM d, yyyy")}</div>
          </div>
        </TooltipContent>
      </Tooltip>
    )
  }

  if (error) {
    return (
      <Card className="border-none shadow-none bg-transparent">
        <Alert variant="destructive" className="bg-transparent border-none px-0">
          <AlertCircle className="h-4 w-4" />
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      </Card>
    )
  }

  return (
    <TooltipProvider delayDuration={0}>
      <Card className="w-full mx-auto max-w-3xl border-none shadow-none bg-transparent">
        <CardHeader className="px-0 pt-0 pb-4">
          <div className="flex items-center justify-between">
            <div className="space-y-1">
              <CardTitle className="text-xl font-bold tracking-tight">Github Contributions</CardTitle>
              {!isLoading && (
                <CardDescription>
                  <span className="font-medium text-foreground">{total}</span> contributions in the last year
                </CardDescription>
              )}
            </div>
            <Button variant="ghost" size="icon" asChild className="h-8 w-8">
              <Link href={`https://github.com/${targetUsername}`} target="_blank">
                <ExternalLink className="h-4 w-4 text-muted-foreground transition-colors hover:text-foreground" />
              </Link>
            </Button>
          </div>
        </CardHeader>
        <CardContent className="px-0 pb-0 mx-auto">
          {isLoading ? (
            <div className="space-y-2 min-w-2xl">
              <Skeleton className="h-36 w-full rounded-md opacity-50" />
              <div className="flex gap-2"><Skeleton className="h-4 w-24 opacity-50" /><Skeleton className="h-4 w-8 opacity-50" /></div>
            </div>
          ) : (
            <div className="w-full overflow-x-auto pb-2 scrollbar-hide">
              <div className="min-w-[700px]">
                <ActivityCalendar
                  data={data}
                  theme={colorTheme}
                  colorScheme={currentTheme === "dark" ? "dark" : "light"}
                  blockRadius={3}
                  blockSize={12}
                  blockMargin={4}
                  fontSize={12}
                  hideColorLegend={false}
                  renderBlock={renderBlock}
                  labels={{ legend: { less: "Less", more: "More" } }}
                />
              </div>
            </div>
          )}
        </CardContent>
      </Card>
    </TooltipProvider>
  )
}

export default GithubHeatMap
03

Use in Your Application

Import and add the component to your Next.js pages or components.

import { GithubHeatMap } from "@/components/ui/github-heatmap"

export default function Page() {
  return <GithubHeatMap username="rimu-7" />
}

Component Code

"use client"

import { useEffect, useState } from "react"
import { ActivityCalendar, type Activity, type ThemeInput } from "react-activity-calendar"
import { ExternalLink, AlertCircle } from "lucide-react"
import Link from "next/link"
import { useTheme } from "next-themes"
import { format } from "date-fns"
import * as React from "react"
import axios from "axios"

import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"

export interface GithubContributionsProps {
  username?: string
}

export interface ContributionDay {
  date: string
  count: number
  level: 0 | 1 | 2 | 3 | 4
}

export interface ApiResponse {
  total: {
    [year: number]: number
    [year: string]: number
  }
  contributions: ContributionDay[]
}

export function GithubHeatMap({ username }: GithubContributionsProps) {
  const { theme, systemTheme } = useTheme()

  const targetUsername = username || process.env.NEXT_PUBLIC_GITHUB_USERNAME || "rimu-7"

  const [data, setData] = useState<ContributionDay[]>([])
  const [total, setTotal] = useState(0)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [prevUsername, setPrevUsername] = useState<string>(targetUsername)

  if (targetUsername !== prevUsername) {
    setPrevUsername(targetUsername)
    setIsLoading(true)
    setError(null)
  }

  useEffect(() => {
    let isMounted = true
    const controller = new AbortController()

    const loadData = async () => {
      if (!targetUsername) return

      try {
        const response = await axios.get<ApiResponse>(
          `https://github-contributions-api.jogruber.de/v4/${targetUsername}?y=last`,
          { signal: controller.signal }
        )

        if (!isMounted) return

        const responseData = response.data

        if (!responseData?.contributions) {
          setData([])
          setTotal(0)
          return
        }

        setData(responseData.contributions)
        setTotal(responseData.contributions.reduce((sum, day) => sum + day.count, 0))
      } catch (err: any) {
        if (!isMounted || axios.isCancel(err)) return

        console.error("Github API Error:", err)
        if (err.response?.status === 404) {
          setError("GitHub user not found")
        } else {
          setError(err.message || "An API error occurred")
        }
      } finally {
        if (isMounted) setIsLoading(false)
      }
    }

    loadData()

    return () => {
      isMounted = false
      controller.abort()
    }
  }, [targetUsername])

  const currentTheme = theme === "system" ? systemTheme : theme
  const colorTheme: ThemeInput = {
    light: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
    dark: ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
  }

  const renderBlock = (block: React.ReactElement, activity: Activity) => {
    const triggerItem = React.cloneElement(block as React.ReactElement<any>, {
      className: "cursor-pointer hover:opacity-80 transition-opacity",
    })

    return (
      <Tooltip key={activity.date}>
        <TooltipTrigger asChild>{triggerItem}</TooltipTrigger>
        <TooltipContent side="top" className="bg-popover text-popover-foreground shadow-md border">
          <div className="text-xs text-center">
            <div className="font-bold">{activity.count === 0 ? "No" : activity.count} contributions</div>
            <div className="text-muted-foreground">{format(new Date(activity.date), "MMM d, yyyy")}</div>
          </div>
        </TooltipContent>
      </Tooltip>
    )
  }

  if (error) {
    return (
      <Card className="border-none shadow-none bg-transparent">
        <Alert variant="destructive" className="bg-transparent border-none px-0">
          <AlertCircle className="h-4 w-4" />
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      </Card>
    )
  }

  return (
    <TooltipProvider delayDuration={0}>
      <Card className="w-full mx-auto max-w-3xl border-none shadow-none bg-transparent">
        <CardHeader className="px-0 pt-0 pb-4">
          <div className="flex items-center justify-between">
            <div className="space-y-1">
              <CardTitle className="text-xl font-bold tracking-tight">Github Contributions</CardTitle>
              {!isLoading && (
                <CardDescription>
                  <span className="font-medium text-foreground">{total}</span> contributions in the last year
                </CardDescription>
              )}
            </div>
            <Button variant="ghost" size="icon" asChild className="h-8 w-8">
              <Link href={`https://github.com/${targetUsername}`} target="_blank">
                <ExternalLink className="h-4 w-4 text-muted-foreground transition-colors hover:text-foreground" />
              </Link>
            </Button>
          </div>
        </CardHeader>
        <CardContent className="px-0 pb-0 mx-auto">
          {isLoading ? (
            <div className="space-y-2 min-w-2xl">
              <Skeleton className="h-36 w-full rounded-md opacity-50" />
              <div className="flex gap-2"><Skeleton className="h-4 w-24 opacity-50" /><Skeleton className="h-4 w-8 opacity-50" /></div>
            </div>
          ) : (
            <div className="w-full overflow-x-auto pb-2 scrollbar-hide">
              <div className="min-w-[700px]">
                <ActivityCalendar
                  data={data}
                  theme={colorTheme}
                  colorScheme={currentTheme === "dark" ? "dark" : "light"}
                  blockRadius={3}
                  blockSize={12}
                  blockMargin={4}
                  fontSize={12}
                  hideColorLegend={false}
                  renderBlock={renderBlock}
                  labels={{ legend: { less: "Less", more: "More" } }}
                />
              </div>
            </div>
          )}
        </CardContent>
      </Card>
    </TooltipProvider>
  )
}

export default GithubHeatMap

Props Reference

PropTypeDefaultDescription
usernamestringNEXT_PUBLIC_GITHUB_USERNAME || "rimu-7"Target GitHub username to fetch and display contribution activity chart.

Usage Examples

basic
<GithubHeatMap username="rimu-7" />
env Fallback
import { GithubHeatMap } from "@/components/ui/github-heatmap"

export default function Profile() {
  const username = process.env.NEXT_PUBLIC_GITHUB_USERNAME || "rimu-7"

  return <GithubHeatMap username={username} />
}
custom Container
<div className="rounded-xl border bg-card p-6 shadow-sm">
  <GithubHeatMap username="vercel" />
</div>