Skip to content
omaris

Data Display

Contribution Graph

The activity calendar: one square per day, a year across.

import { ContributionGraph } from 'omaris'
Learn

Examples

Basic

A year of activity, one square a day. Hover a square for its date.

1,409 commits 210 active days Longest streak 7 Current streak 1
Less More
<script lang="ts">	import { ContributionGraph, type ActivityDay } from 'omaris';​	const days: ActivityDay[] = Array.from({ length: 365 }, (_, index) => {		const date = new Date(Date.now() - (364 - index) * 86_400_000);		const weekend = date.getDay() === 0 || date.getDay() === 6;		const noise = Math.sin(index * 12.9898) * 43758.5453;		const chance = noise - Math.floor(noise);		return { date, count: chance > (weekend ? 0.7 : 0.35) ? Math.ceil(chance * 9) : 0 };	});</script>​<div class="w-full">	<ContributionGraph data={days} unit="commit" stats /></div>

Narrow

The same year in a phone-width column. trim drops the oldest weeks until what is left fits; scroll keeps them all and opens on the most recent.

Trimmed to fit
Less More
Scrolls
Less More
<script lang="ts">	import { ContributionGraph, type ActivityDay } from 'omaris';​	const days: ActivityDay[] = Array.from({ length: 365 }, (_, index) => {		const date = new Date(Date.now() - (364 - index) * 86_400_000);		const noise = Math.sin(index * 7.331) * 15731.7;		const chance = noise - Math.floor(noise);		return { date, count: chance > 0.4 ? Math.ceil(chance * 7) : 0 };	});</script>​<div class="flex w-full max-w-72 flex-col gap-6">	<ContributionGraph data={days} fit="trim" size="sm" title="Trimmed to fit" />	<ContributionGraph data={days} fit="scroll" size="sm" title="Scrolls" /></div>

Tones

The scale is mixed from one tone, so the graph matches whatever it tracks.

531 workouts
531 shifts
<script lang="ts">	import { ContributionGraph, type ActivityDay } from 'omaris';​	const days: ActivityDay[] = Array.from({ length: 182 }, (_, index) => {		const date = new Date(Date.now() - (181 - index) * 86_400_000);		const noise = Math.sin(index * 3.77) * 9731.3;		const chance = noise - Math.floor(noise);		return { date, count: chance > 0.35 ? Math.ceil(chance * 6) : 0 };	});</script>​<div class="flex w-full flex-col gap-6">	<ContributionGraph data={days} tone="success" unit="workout" weeks={26} legend={false} />	<ContributionGraph data={days} tone="tertiary" unit="shift" weeks={26} legend={false} /></div>

Overridden

Round cells, a fixed scale and a header of your own.

765 across 96 days
Less More
<script lang="ts">	import { ContributionGraph, type ActivityDay } from 'omaris';​	const days: ActivityDay[] = Array.from({ length: 140 }, (_, index) => {		const date = new Date(Date.now() - (139 - index) * 86_400_000);		const noise = Math.sin(index * 5.13) * 22331.9;		const chance = noise - Math.floor(noise);		return { date, count: chance > 0.3 ? Math.ceil(chance * 12) : 0 };	});</script>​<div class="w-full">	<ContributionGraph		data={days}		weeks={20}		thresholds={[1, 3, 6, 10]}		classes={{ cell: 'rounded-full', legend: 'justify-start' }}	>		{#snippet header({ total, days: active })}			<div class="flex items-baseline gap-2">				<span class="text-2xl font-semibold tabular-nums">{total}</span>				<span class="text-sm text-muted-foreground">across {active} days</span>			</div>		{/snippet}	</ContributionGraph></div>

When to use it

Use it for

  • Something that happens some days and not others, over months: commits, workouts, shifts, orders shipped, doses taken. It answers "how consistent has this been" faster than a line chart.
  • The header of a profile, a project page or a habit tracker, where a year has to fit above the fold.
  • Spotting gaps. Two empty weeks are visible at a glance and invisible in a table.
  • A quarter or a month, with weeks={13}. Same picture, shorter range, easier to read on a phone.

Not for

  • A value you need to read rather than compare, like revenue or temperature → a Chart. This has five levels.
  • Fewer than about eight weeks. A handful of squares is a table.
  • Picking a date → Picker or a calendar. The squares are a report, even when onselect makes them clickable.
  • Comparing two series. One graph shows one thing.

Do

  • Set unit to the thing counted: "commit", "workout", "shift". It appears in the title and every tooltip.
  • Choose fit for the space: trim for a narrow card that should show fewer weeks, scroll when the whole year matters.
  • Leave thresholds alone unless the scale is known and fixed. The defaults are the data's own quartiles, so a quiet year reads as well as a busy one.
  • Use tone to match what it tracks: success for a habit, info for deliveries, destructive for incidents.
  • Turn stats on only where streaks are the point; a streak counter changes how people feel about a gap. Keep the legend on the first graph on a page and drop it on repeats.

Don't

  • Show more than about 53 weeks. Past a year the squares stop being countable.
  • Put a year of size="lg" squares in a phone-width card and let it scroll sideways. Trim it instead.
  • Use it for something that happens every day. A graph with no empty squares says nothing.
  • Rely on colour alone. Every square carries its date and count as its accessible name and tooltip; leave those in place.

Quick reference

size
  • sm
  • md (default)
  • lg

API

ContributionGraph

The activity calendar: one square per day, a year across.

It is the clearest way to show when something happened over a long stretch — commits, workouts, shifts, deliveries, sales. A chart shows the shape of a trend; this shows the habit, the gaps and the streaks, and it does it in a block small enough to sit at the top of a page.

On a narrow screen there are two honest answers and it takes both: fit="trim" drops the oldest weeks until the rest fit the container exactly, and fit="scroll" keeps the year and scrolls it, opening at the most recent week.

import { ContributionGraph } from 'omaris'
<ContributionGraph data={days} tone="success" fit="trim" stats />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The tooltip hangs off this, so it is the positioned ancestor.
header
The title row above the grid.
title
"1,248 contributions in the last year".
stats
The three figures under stats.
board
The weekday column and the scroller, side by side.
scroller
The scroller the grid lives in — the weekday names stay outside it, so they are still there once it has been scrolled. The padding is room for a focus ring on an edge square; the negative margins hand that room back to the layout.
frame
The month names and the grid, stacked.
weekdays
The column of weekday names, aligned with the grid's rows.
months
The row of month names.
grid
The squares.
cell
One square — the shape and the colour, with no interaction in it.
legend
"Less ▢▢▢▢▢ More".
tooltip
The floating label. Positioned against the root, so nothing clips it.

Props

data

Defaults to []

ActivityDay[]

One entry per day that has anything. Missing days are empty ones.

to
Date

Last day on the grid. Defaults to today.

from
Date

First day. Defaults to weeks back from to.

weeks

Defaults to 53

number

How far back to go when there is no from.

fit

Defaults to 'scroll'

ContributionFit

What happens when the grid is wider than its container.

size

Defaults to 'md'

ContributionGraphSize
sm
md
lg
tone

Defaults to 'primary'

Tone

The colour the levels are mixed from.

weekStart

Defaults to 0

0 | 1 | 2 | 3 | 4 | 5 | 6

Day the week starts on — 0 Sunday, 1 Monday.

thresholds
[number, number, number, number]

Counts at which each level starts, ascending, four of them. Left out, they are the quartiles of the data — so a graph of a busy year and one of a quiet year are both readable.

months

Defaults to true

boolean

Draw the month names above the grid.

weekdays

Defaults to true

boolean

Draw the weekday names beside it.

legend

Defaults to true

boolean

Draw the Less–More key under it.

stats

Defaults to false

boolean

Show the total, the longest run and the current one.

title
string

Replaces the built-in "N contributions in …" title.

unit

Defaults to 'contribution'

string

The noun in the title and every tooltip — "commit", "workout".

locale
string

BCP-47 tag for the month, weekday and date names.

onselect
(day: ActivityCell) => void

Fires when a day is clicked or activated from the keyboard.

day
Snippet<[ActivityCell]>

Replaces a square. Gets the day and its level.

header
Snippet<[{ total: number; days: number }]>

Replaces the header row.

class
string
classes
ContributionGraphClasses

Per-part Tailwind overrides. class still covers the root.