shadcn.io is not affiliated with official shadcn/ui
Habit Tracker Dialog Block for Ark UI
A habit app's Today page with its weekly log shown open: four habits by seven days of squares to tick, arrow keys between them, a streak for each habit, a total for each day and the week so far, with Today 3 of 4 carried to the page behind.
Installation
Overview
Log a week of small habits in one place, with this habit tracker dialog for React with Ark UI. It opens from the Today page of Dayrung, a habit app: the page's header holds an outline Log today Button, and the page, a "Today 3 of 4" figure over four rings and a few lines, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The Dialog holds a table of four habits by the seven days of 5 to 11 October 2026. Each day is a square you press to tick, filled with a check when done, days after the fixed "today" are disabled, and today is ringed. The arrow keys, Home and End move between squares. Each habit has a streak counted back from today through four typed earlier weeks, each day has a total ("3/4"), and a Progress bar gives the week so far. Ticking recomputes all of it, and the "Today" figure and the rings on the page behind follow. For preview the dialog is drawn open in place when the page loads.
FAQ
Why is the dialog already open?
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 Log today button in the header 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 WeekGrid component and share the block's state, so a square ticked in place is ticked 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.
How is a streak counted?
streakOf joins the four earlier weeks and this week up to today into one run of days and counts the ticked days backwards from the end. If today is not ticked yet the count starts from yesterday, so a streak is not lost before the day is over. A habit with nothing to count says "No streak yet". The count updates the moment a square changes, and the polite status says the new streak, the day and the week's percentage.
How do the squares work with a keyboard?
Each square is a native button with aria-pressed, named for its habit and day ("Walk 30 minutes, Friday 9 Oct"). Only one square is in the tab order, the one you last used (today's first habit to begin with), and the arrow keys move between squares, mirrored in a right-to-left page, with Home and End jumping to the first and last enabled day of the row. Space or Enter ticks. Future days are disabled and skipped.
What stays local, and how do I wire it for real?
Everything. The habits and the four earlier weeks are typed into the file, AS_OF and TODAY are the fixed day that counts as now, and ticks live in this page's state. In your app load the week from your API, post each tick from tick, derive TODAY from the person's own date and time zone, and keep the streak rule on the server so every device agrees.
Where does focus go?
The real dialog is named by its title and described by its sentence, and focus starts on the grid's one tab stop. Pressing a square keeps focus on it. Escape and Done return focus to the Log today button. The trigger is wrapped in DialogTrigger with asChild, and the dialog is controlled with open and onOpenChange.
How does it look on a phone?
The copy shown on load docks to the foot of the frame as a bottom sheet with Done pinned and the grid scrolling above it. The day squares shrink a little, the habit names wrap, and the streak moves from its own column to a line under each habit's name. The real dialog keeps the Dialog's own phone layout, a sheet from the bottom.