Retention Curve
What fraction of each cohort is still here, period by period — normalised so growth cannot hide churn. Later cohorts are drawn shorter, not padded with zeroes.
src/components/ui/retention-curve.tsxAbout
From the component's own source, where it was written.
What fraction of a cohort is still here, period by period.
**The one question this answers that a line of active users cannot:** whether the product keeps people. Total actives goes up while retention collapses, as long as acquisition outruns churn — which is exactly what a business looks like just before it stops working. Retention normalises the cohort to its own starting size, so growth cannot hide inside it.
**Curves that flatten are the signal.** A curve decaying toward a horizontal asymptote means a stable core formed; one still falling at the right-hand edge means it has not, and the product has no floor yet. That shape — not the day-1 number everyone quotes — is what the chart is for, which is why the flattening point is called out rather than left to be eyeballed.
**Later cohorts have fewer observed periods, and this shows it** by drawing them shorter instead of extending them to the axis. A curve padded with zeroes for periods that have not happened yet reads as catastrophic churn, and it is the most common way this chart lies.
Values are fractions (0–1) of each cohort's own starting size; period 0 is therefore always 1 and is drawn, because its absence makes the first drop hard to read.
Install
npx astralyx-ui add retention-curve
Usage
import { RetentionCurve } from '@/components/ui/retention-curve'
<RetentionCurve cohorts={cohorts} periodLabel={(n) => `Week ${n}`} />Composer
Change the props and the source below follows.
Flattens at M5, around 32% — the shape of a product that has found a core.
- Jan4,210
- Feb5,040
- Mar6,180
- Apr7,320
- May8,050
<RetentionCurve
cohorts={cohorts}
showFlatten={true}
periodLabel={(n) => `M${n}`}
/>Examples
Five monthly cohorts
<RetentionCurve cohorts={cohorts} periodLabel={(n) => `M${n}`} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
RetentionCurve
Extends Omit<ComponentProps<'figure'>, 'height'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| cohorts | Cohort[] | required | Padding unobserved periods with zeroes reads as catastrophic churn. It is the most common way this chart lies. |
| height | number | 260 | — |
| periodLabel | (index: number) => string | (index) => `P${index}` | — |
| flattenThreshold | number | 0.02 | Period-over-period drop below which the curve counts as flat. |
| showFlatten | boolean | true | — |
| legend | boolean | true | — |
| emptyLabel | string | 'No cohorts.' | — |
| label | string | 'Retention curve' | — |
| footnote | ReactNode | — | — |
Types
Cohort
What fraction of a cohort is still here, period by period.
| Field | Type | Description |
|---|---|---|
| name | string | "2026-03", "Week 12" — whatever labels the group. |
| size? | number | Users in this cohort at period 0. |
| values | number[] | Fraction retained per period, starting at period 0 (which is 1). |
| color? | string | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| why not active users | growth hides churn | Total actives rises while retention collapses, as long as acquisition outruns churn — which is what a business looks like just before it stops working. |
| flattening | the real signal | A curve decaying to a horizontal asymptote means a stable core formed; one still falling at the right edge means there is no floor yet. That shape, not the day-1 number. |