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.tsx

About

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.

Retention curve
Jan (4,210 users): M0 100%, M1 52%, M2 41%, M3 36%, M4 33%, M5 32%, M6 31%, M7 31%Feb (5,040 users): M0 100%, M1 55%, M2 44%, M3 39%, M4 36%, M5 35%, M6 34%Mar (6,180 users): M0 100%, M1 58%, M2 47%, M3 42%, M4 40%, M5 39%Apr (7,320 users): M0 100%, M1 61%, M2 50%, M3 46%, M4 44%May (8,050 users): M0 100%, M1 63%, M2 53%, M3 49%
100%0%
M0M7

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
Props
tsx
<RetentionCurve
  cohorts={cohorts}
  showFlatten={true}
  periodLabel={(n) => `M${n}`}
/>

Examples

Five monthly cohorts

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'>.

PropTypeDefaultDescription
cohortsCohort[]requiredPadding unobserved periods with zeroes reads as catastrophic churn. It is the most common way this chart lies.
heightnumber260—
periodLabel(index: number) => string(index) => `P${index}`—
flattenThresholdnumber0.02Period-over-period drop below which the curve counts as flat.
showFlattenbooleantrue—
legendbooleantrue—
emptyLabelstring'No cohorts.'—
labelstring'Retention curve'—
footnoteReactNode——

Types

Cohort

What fraction of a cohort is still here, period by period.

FieldTypeDescription
namestring"2026-03", "Week 12" — whatever labels the group.
size?numberUsers in this cohort at period 0.
valuesnumber[]Fraction retained per period, starting at period 0 (which is 1).
color?string—

Behaviour

TopicDetailDescription
why not active usersgrowth hides churnTotal actives rises while retention collapses, as long as acquisition outruns churn — which is what a business looks like just before it stops working.
flatteningthe real signalA 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.