shadcn.io is not affiliated with official shadcn/ui
Streak Tracker Dialog Block for React
A habit app's list with a daily check-in dialog shown open: a flame and the days in a row, a five-week calendar with this week's days large, milestones at 7, 14 and 30, and a check-in that moves the number.
Installation
Overview
Keep people coming back one day at a time, with this streak dialog for React with Base UI. It opens from the Habits home of Rungday, a habit app: the page's header holds an outline Daily check-in Button, and the habit list, three rows each with a small streak badge, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The Dialog shows a flame beside the days in a row, a Sliding Number that rolls when it changes, and one line: Check in today to make it 12. Under it is a calendar of the last five weeks, 35 days in a list where this week's seven are the big cells: done is a filled square with a check, today is ringed and the days to come are dashed. Three milestones, 7, 14 and 30 days, are checked once reached, with a bar and "3 days to 14". Check in today, the only Shine Button, fills today's cell, the number goes to 12, the line says to come back tomorrow and the button reads Checked in; the drawn habit row shows the new number. For preview the dialog is drawn open in place when the page loads; the header button opens the real Dialog, with the same calendar and the same state. For habit trackers, learning apps, fitness apps and any daily ritual.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the page: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Daily check-in button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same StreakBody component and share the block's state, so a check-in made in the copy is there in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
Where does the streak number come from?
From a typed log of the days that are done, never a counter: the streak is the run of consecutive days that ends today, or yesterday while today isn't done yet, found by streakOf. Every date comes from one fixed day, AS_OF, so nothing ticks and the preview never ages; 28 September to 8 October is 11 days, and a miss on 27 September ends the run before it. Checking in adds today to the log, so the number can only change when you press the button, and the milestones and the "days to" line are worked out from it.
How is the calendar built, and how does a screen reader read it?
Thirty-five days in a list, Monday first, ending on the Sunday of the current week: the first four weeks are slim cells and the fifth, this week, is the large row with the weekday letter in each cell. State is a shape and a word, never a colour: done is filled, a missed day is an empty outline, today is ringed, and a day to come is dashed. Each cell carries its name for assistive technology, like "Thu 8 Oct, done" or "Fri 9 Oct, today, not yet", and the list reads left to right in both text directions.
What happens when I press Check in today?
Today's cell fills with a check, the number rolls from 11 to 12 (with reduced motion it changes in place), the milestone line says "2 days to 14", a polite message says "Checked in. 12 days in a row" and the button reads Checked in, quiet but still pressable: pressing it again says you already checked in. The dialog stays open to show the result; Later and Escape close it, and the drawn habit row behind it carries the new number.
Does it track anything, and how do I wire it up?
No. The log lives in this tab and a polite message says "in this demo" and "Nothing was tracked". In your app read the log from your server, send the check-in from checkIn, and take the new streak from the response: let the server decide what a day is, in the person's own time zone, and whether a streak can be frozen. Replace AS_OF with today's date and keep streakOf.
Is it accessible from the keyboard?
Yes. The real dialog is named by its title and described by its sentence, focus starts inside it, and Escape, Later and the close button all return focus to the header button. The trigger is passed to DialogTrigger through render, and the dialog is controlled with open and onOpenChange. The number is read as text ("11 days") and one polite status message reports what changed. The copy shown on load moves no focus; Escape inside it, Later and its close button close it and put focus on the header button.
Related
- Pro
Activity Streak Stats
HeatmapsA 26-week contribution grid of daily lessons with month labels, a Less to More key and a band of three counters for the current streak, the longest streak and the active days.
- Pro
A panel for a reading streak kept alive: the streak counted up to 14 days, a progress bar to the Weekly Warrior badge, and a five-week table of days read, missed and still to come.
- Pro
A habit-app streak notice: the run in a rolling number, this week as seven day cells where today fills in when you press Mark today done, an Undo, and a Streak freeze switch that says in words what a missed day would do.
- Pro
Meeting RSVP Dialog
Time and schedulingA team calendar's week with its reply dialog shown open: the event, a three-way Going, Maybe or Can't go answer, an optional note, and the guests by answer with their counts, moving as you choose.
- Pro
Pomodoro Timer Dialog
Time and schedulingA focus timer's Today page with its dialog shown open: a Focus, Short break and Long break switch, a ring that fills with the digits inside it, Start and Pause, four session dots and durations that apply at the next start.
- Pro
Schedule Meeting Dialog
Time and schedulingA month page with its new event dialog shown open: a title, a day from a folded calendar, a start time and length, guest emails as tags, and a day timeline that moves with the time and warns when the meeting clashes.