Habit tracker

Shows dated complete, missed, skipped, and unrecorded states, with controlled completion actions and a responsive week view.

Evening walk

  • Friday
  • Saturday
  • Sunday

Make room for a walk

7–13 September 2026

Evening walk

Mark a day complete, or clear a completed day.

  • Mon 7
  • Tue 8
  • Wed 9
  • Thu 10
  • Fri 11
  • Sat 12
  • Sun 13

This example keeps changes only while the page is open.

Use it for

  • A week of application-owned habit records, supplied in the intended order.
  • Explicit date labels and stable date keys, with no inferred current day or timezone.
  • onDayChange to request complete or clear completion to unrecorded; the application updates days and owns persistence.

Not for

  • Inferring missed days from absent records or calculating a punitive streak.
  • Assuming a click has been saved without updating and persisting the supplied data.

Three ways in. They share one source, so the pixels match whichever you pick.

1. Import the package

npm install @noorddev/vlak-react

// once, next to your app's root
import "@noorddev/vlak-react/css";

import { HabitTracker } from "@noorddev/vlak-react";

Precompiled React and one stylesheet. No compiler to configure. Per-component imports work too: @noorddev/vlak-react/components/habit-tracker.

2. Vendor the source

npx @noorddev/vlak-cli add habit-tracker

The StyleX leaf lands in components/vlak/, for your own compiler to own. See StyleX.

3. Through shadcn

npx shadcn add https://vlak.dev/r/habit-tracker.json

The same registry item, installed by shadcn's CLI.

CSS only

<link rel="stylesheet" href="node_modules/@noorddev/vlak/css/vlak.css" />

No React. Link vlak.css and use the markup and classes below.

import { useState } from "react";
import { HabitTracker } from "@noorddev/vlak-react";
import type { HabitDay } from "@noorddev/vlak-react";

function WalkingRecord() {
  const [days, setDays] = useState<HabitDay[]>([
    { date: "2026-09-07", label: "Mon 7 Sep", status: "complete" },
    { date: "2026-09-08", label: "Tue 8 Sep", status: "unrecorded" },
  ]);
  return <HabitTracker label="Evening walk" days={days} onDayChange={(date, status) => setDays((records) => records.map((day) => day.date === date ? { ...day, status } : day))} />;
}

HabitTracker

Dated completion records with caller-owned state and no inferred streak.

PropTypeDefaultDescription
labelrequiredReactNode
descriptionReactNode
daysrequiredHabitDay[]A supplied week or another dated range, rendered in the given order.
onDayChange(date: string, status: HabitStatus) => voidA completion action requests complete, or unrecorded when clearing completion.
disabledbooleanfalse
readOnlybooleanfalse

Also accepts HTMLAttributes<HTMLDivElement>.

KeysDoes
TabMoves through enabled completion controls when onDayChange is supplied.
Enter, SpaceRequests completion, or clears an existing completion to unrecorded.
<div class="rs-habit-tracker" role="group" aria-labelledby="habit-label"><p class="rs-habit-tracker-label" id="habit-label">Evening walk</p><ul class="rs-habit-tracker-days"><li class="rs-habit-tracker-day"><span class="rs-habit-tracker-date">Mon 7 Sep</span><div class="rs-habit-tracker-control rs-habit-tracker-complete rs-habit-tracker-static"><span class="rs-habit-tracker-mark" aria-hidden="true">✓</span><span>Complete</span></div></li><li class="rs-habit-tracker-day"><span class="rs-habit-tracker-date">Tue 8 Sep</span><div class="rs-habit-tracker-control rs-habit-tracker-static"><span class="rs-habit-tracker-mark" aria-hidden="true">·</span><span>Unrecorded</span></div></li></ul></div>
.rs-habit-tracker.rs-habit-tracker-label.rs-habit-tracker-description.rs-habit-tracker-days.rs-habit-tracker-day.rs-habit-tracker-date.rs-habit-tracker-control.rs-habit-tracker-complete.rs-habit-tracker-unavailable.rs-habit-tracker-static.rs-habit-tracker-mark