Data Display
Contribution Graph
The activity calendar: one square per day, a year across.
import { ContributionGraph } from 'omaris' Examples
Basic
A year of activity, one square a day. Hover a square for its date.
<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.
<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.
<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.
<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
onselectmakes them clickable. - Comparing two series. One graph shows one thing.
Do
- Set
unitto the thing counted: "commit", "workout", "shift". It appears in the title and every tooltip. - Choose
fitfor the space:trimfor a narrow card that should show fewer weeks,scrollwhen the whole year matters. - Leave
thresholdsalone 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
toneto match what it tracks:successfor a habit,infofor deliveries,destructivefor incidents. - Turn
statson only where streaks are the point; a streak counter changes how people feel about a gap. Keep thelegendon 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 smmd(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 smmdlg
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.