KQ-1316 · COHORT/GRID

Cohort Grid

A cohort retention triangle built from weekly cohorts: cells fill in on snap along the diagonals, the rows or the columns, the whole sweep inside 600ms, and a Users / Revenue switch slides its thumb on snap and recolours the grid in the same order. Pointing at a cell or stepping with the arrow keys lays a row band and a column band through it that ride between cells on snap, while the curve above, sharing the grid's week columns, morphs from the size-weighted average to that cohort on glide and carries on as a dashed projection past the weeks it has lived. It is a real grid with one tab stop: arrows walk the triangle, Home and End reach a row's ends, Enter pins a cohort and Escape lets it go.

KQ-1316 · COHORT/GRID

Fieldline retention

10 weekly cohorts · Jul 20 to Sep 21 · users retained

CohortW0W1W2W3W4W5W6W7W8W9
Jul 20
Jul 27
Aug 3
Aug 10
Aug 17
Aug 24
Aug 31
Sep 7
Sep 14
Sep 21

week 1 avg 48% · nothing pinned · users retained

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/cohort-grid

Props

cascade
"diagonal" | "rows" | "columns"
default "diagonal"
The order the cells fill in and recolour in. Changing it replays the fill.
metric / defaultMetric
"users" | "revenue"
default "users"
What the cells measure: users retained as a share of the cohort, or revenue as a share of week 0 (which can pass 100%). Controlled or uncontrolled.
onMetricChange
(metric: "users" | "revenue") => void
default —
Fires from the switch or arrow key that changed the metric.
curve
"area" | "line" | "off"
default "area"
The summary curve over the grid: filled, a line, or none.
cohorts
Cohort[]
default defaultCohorts
One per week of sign-ups, oldest first: id, start (YYYY-MM-DD), size, and users and revenue per week since.
weeks
number
default —
Week columns shown. Defaults to the longest cohort, at most 12.
now
number | Date
default defaultCohortNow
Today: the week each cohort is living through is hatched in progress and left out of the averages.
selected / defaultSelected
string | null
default null
The pinned cohort: the curve and the side panel hold it when the pointer leaves. Controlled or uncontrolled.
onSelectedChange
(id: string | null) => void
default —
Fires from the click or key that pinned or unpinned a cohort.
onCellSelect
(cohortId: string, week: number) => void
default —
Enter on a cell, or a click on it.
onCrosshairChange
(cell: { cohortId: string; week: number } | null) => void
default —
The crosshair moved to a cell by pointer or key, or left the grid. Use it to link other views.
format
(value: number, metric: "users" | "revenue") => string
default —
A cell's value, a share (0.31 is 31%), as text. Defaults to a whole percent.
formatMoney
(amount: number) => string
default —
Money in the readouts and the side panel. Defaults to whole US dollars, en-US.
formatDate
(iso: string) => string
default —
Cohort labels. Defaults to "Aug 3", in UTC.
status
"ready" | "loading" | "error"
default "ready"
Whether the cohorts have arrived: a skeleton triangle while loading, a message with Try again on error.
onRetry
() => void
default —
Try again was pressed while status is "error".
title
string
default "Retention"
The grid's heading; also its accessible name unless label is given.
label
string
default —
The grid's accessible name. Defaults to the title.
sound
boolean
default false
A tick for each cell you step to or click, pitched by its value, and one for a metric switch.
disabled
boolean
default false
Read only: cells still read out; nothing pins and the metric is fixed.
className
string
default —
Classes for the root. It is at most 560px tall and scrolls inside itself.