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