Cohort Table

Retention by signup period, as a triangle. Cells are shaded by rate rather than by count, so a small cohort that retains well is not washed out by a large one that does not.

src/components/ui/cohort-table.tsx

About

From the component's own source, where it was written.

A retention cohort grid — signup period down, periods since across.

Incomplete cohorts are marked rather than plotted. The newest cohort has not lived long enough to have a week-4 number, and rendering an empty cell as 0% makes retention look like it fell off a cliff. This is the single most common way a cohort chart lies.

Period 0 is always 100% by definition, so it is rendered as the cohort size instead — that cell is the only place the absolute number appears, and without it every percentage is unanchored.

Install

npx astralyx-ui add cohort-table

Usage

import { CohortTable } from '@/components/ui/cohort-table'

<CohortTable cohorts={cohorts} periods={7} />

Composer

Change the props and the source below follows.

CohortSize0123456
Mar 20261,284100%58%46%39%36%34%33%
Apr 20261,609100%62%49%43%39%38%·
May 20261,412100%59%46%40%36%··
Jun 20262,058100%67%54%48%···
Jul 20262,240100%67%53%····
Aug 20261,977100%64%·····
Sep 2026604100%······
Average100%63%50%42%37%36%33%

A dot means the cohort has not reached that period yet — not zero retention. Averages count only cohorts that have.

Props
tsx
<CohortTable cohorts={cohorts} periods={7} />

Examples

Weekly retention

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

CohortTable

Extends ComponentProps<'div'>.

PropTypeDefaultDescription
cohortsCohort[]required{ label, size, values } where values[0] is the cohort period. Later periods may be missing — a cohort three months old has no month-six figure yet.
periodsnumber—Columns rendered. Rows shorter than this end where their data does.
periodLabel(index: number) => ReactNodeDEFAULT_PERIOD_LABELFor weeks, days, or whatever the cohort period actually is.
localestring'en-GB'—
cohortHeaderReactNode'Cohort'—
sizeHeaderReactNode'Size'—
averageLabelReactNode'Average'Label on the averages row.
notReachedLabelstring'Not yet reached'Accessible name for a period a cohort has not reached.
footnoteReactNode'A dot means the cohort has not reached that period yet — not zero retention. Averages count only cohorts that have.'Note under the table. Pass null to drop it.

Types

Cohort

A retention cohort grid — signup period down, periods since across.

FieldTypeDescription
labelReactNodePeriod label — "Jan 2026", "Week 12".
sizenumberPeople in the cohort at period 0.
values(number | null)[]Retained counts per period, index 0 being the cohort period.

Behaviour

TopicDetailDescription
missing vs zeronullA null renders as an empty cell, never as 0%. Drawing "not yet measured" as total churn is the classic cohort-chart lie.
shadingby rateEach cell is a share of its own cohort size, so rows are comparable across wildly different cohort sizes.