# Habit Streak Calendar

> A month calendar where each click marks a day done or not, computing the current and the best streak and refusing future days. Use this when a habit or practice is logged day by day and the run of consecutive days matters.

A shadcn/ui block from the [@tealmac](https://ui.tealmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/tealmac/habit-streak-calendar).

- Page, with a live preview: https://shadcn.exchange/r/tealmac/habit-streak-calendar
- Screenshot: https://shadcn.exchange/shot/tealmac/habit-streak-calendar/light.png
- Registry item: https://ui.tealmac.com/r/habit-streak-calendar.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/tealmac/habit-streak-calendar.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@tealmac/calendar-grid`, `@tealmac/date-time-core`

## Files

### `registry/patterns/habit-streak-calendar/habit-streak-calendar.tsx`

```tsx
"use client"

import * as React from "react"

import { CalendarGrid } from "@/components/ui/calendar-grid"
import {
  addDays,
  resolveLocale,
  today as todayISO,
} from "@/components/ui/date-time-core"

export interface Streak {
  current: number
  best: number
}

export interface HabitStreakCalendarProps {
  value?: string[]
  defaultValue?: string[]
  onValueChange?: (days: string[], streak: Streak) => void
  today?: string
  locale?: string
}

/** Current streak (ending today, or yesterday if today is not done yet) and the best ever. */
export function streaks(days: string[], today: string): Streak {
  const set = new Set(days)
  let best = 0
  let run = 0
  const sorted = [...set].sort()
  sorted.forEach((d, i) => {
    run = i > 0 && addDays(sorted[i - 1], 1) === d ? run + 1 : 1
    best = Math.max(best, run)
  })
  let current = 0
  let d = set.has(today) ? today : addDays(today, -1)
  while (set.has(d)) {
    current++
    d = addDays(d, -1)
  }
  return { current, best }
}

export function HabitStreakCalendar({
  value,
  defaultValue = [],
  onValueChange,
  today,
  locale,
}: HabitStreakCalendarProps) {
  const loc = resolveLocale(locale)
  const now = today ?? todayISO()
  const [inner, setInner] = React.useState<string[]>(defaultValue)
  const days = value ?? inner

  function toggle(d: string) {
    const next = days.includes(d) ? days.filter((x) => x !== d) : [...days, d]
    if (value === undefined) setInner(next)
    onValueChange?.(next, streaks(next, now))
  }
  const s = streaks(days, now)

  return (
    <div className="flex flex-col gap-3">
      <CalendarGrid
        label="Habit log"
        locale={loc}
        today={now}
        value={null}
        onValueChange={(d) => d && toggle(d)}
        markers={Object.fromEntries(days.map((d) => [d, "done"]))}
        max={now}
        className="border"
      />
      <p role="status" className="text-sm">
        Current streak:{" "}
        <strong>
          {s.current} day{s.current === 1 ? "" : "s"}
        </strong>
        . Best: {s.best}.
      </p>
    </div>
  )
}
```
