A habit tracker in Obsidian without Dataview
Tick a checkbox in each daily note. Two short blocks turn those ticks into a count, a streak, this week against last week, and a year of days. No Dataview and no JavaScript.
1Give every daily note a checkbox
One checkbox property per habit. In a daily note, add a property, set its type to Checkbox, and name it after the habit.
---
gym: true
meditate: true
read: false
---
## 2026-09-14
2026-09-14The note's name is its date. Anything after the date is fine:2026-09-14 Mondaycounts too.gym: trueA ticked box counts as 1, an unticked one as 0.DiaryThe folder the blocks read from. Use the one your daily notes already live in.
Add the properties, unticked, to your daily note template. Every new day then starts with the boxes ready to tick.
2Count the days
Make a note for the tracker, say Habits.md, and paste this block. It is the top row of the picture above.
```stats
columns: 4
items:
- { label: Gym days, source: Diary, field: gym, agg: sum, icon: 🏋️ }
- { label: Longest gym streak, source: Diary, field: gym, agg: streak, unit: days, icon: 🔥 }
- { label: Gym this week, source: Diary, field: gym, agg: sum, period: week, icon: 📅, compare: true, better: up }
- { label: Gym this month, source: Diary, field: gym, agg: sum, period: month, icon: 🗓 }
```
agg: sumAdds up the ticks, so it counts the ticked days.agg: streakThe longest run of ticked days on record. An unticked day breaks it, and so does a day with no note.period: weekOnly this week, ending today. The week starts on Monday or Sunday, as your language has it.compare: trueSets this week against the same days of last week. Withbetter: up, a rise shows green.
3Paint the year
Paste this under the cards. Every ticked day gets a coloured cell. Click a cell to open that day's note.
```heatmap
source: Diary
field: gym
color: orange
title: Gym
```
Without range you get one grid per calendar year. range: 182d shows the last half year as one grid instead, which fits a phone better.
4The streak you are on
The longest streak is history. For the run you are on today, use current_streak. Here it is on a second habit, meditate:
```stats
columns: 2
items:
- { label: Days in a row, source: Diary, field: meditate, agg: current_streak, unit: days, icon: 🔥 }
- { label: Best streak, source: Diary, field: meditate, agg: streak, unit: days }
```
Today never breaks the run. Until you tick today's box, the count stops at yesterday. Only a missed yesterday ends the run.
The whole note
Everything above in one piece, ready to paste into an empty note.
```stats
columns: 4
items:
- { label: Gym days, source: Diary, field: gym, agg: sum, icon: 🏋️ }
- { label: Longest gym streak, source: Diary, field: gym, agg: streak, unit: days, icon: 🔥 }
- { label: Gym this week, source: Diary, field: gym, agg: sum, period: week, icon: 📅, compare: true, better: up }
- { label: Gym this month, source: Diary, field: gym, agg: sum, period: month, icon: 🗓 }
```
```heatmap
source: Diary
field: gym
color: orange
title: Gym
```
```stats
columns: 2
items:
- { label: Days in a row, source: Diary, field: meditate, agg: current_streak, unit: days, icon: 🔥 }
- { label: Best streak, source: Diary, field: meditate, agg: streak, unit: days }
```
The same tracker with Tracker or dataviewjs
If you searched for this, you have probably seen one of these. Both read the same gym: true notes.
```tracker
searchType: frontmatter
searchTarget: gym
folder: Diary
summary:
template: "Gym days: {{sum()}}\nLongest streak: {{maxStreak()}} days\nCurrent streak: {{currentStreak()}} days"
```
```tracker
searchType: frontmatter
searchTarget: gym
folder: Diary
datasetName: Gym
month:
startWeekOn: 'Mon'
```
- Tracker reads
trueas 1 andfalseas 0, sosum()counts the ticked days. - The calendar is a month view: one month at a time, with arrows to the others.
- The current streak counts back from the newest note Tracker finds, not from today, unless you set
endDate. - Tracker's docs list no setting for "this week against last week".
```dataviewjs
const days = dv.pages('"Diary"')
.where(p => p.file.day && p.gym === true)
.sort(p => p.file.day)
.map(p => p.file.day)
.array();
let best = 0, run = 0, prev = null;
for (const d of days) {
run = prev && d.diff(prev, "days").days === 1 ? run + 1 : 1;
best = Math.max(best, run);
prev = d;
}
dv.paragraph(`Gym days: ${days.length}`);
dv.paragraph(`Longest streak: ${best} days`);
```
- Needs Enable JavaScript queries in Dataview's settings. It is off by default.
- This prints two lines of text. Cards, the week comparison and the year grid are more code, or another plugin.
- Two notes on the same day reset this streak to 1. Fixing that is more code too.
| Dashy | Tracker | dataviewjs | |
|---|---|---|---|
Reads gym: true | Yes | Yes, with searchType: frontmatter | Yes, you compare it in code |
| Count and longest streak | sum, streak | sum(), maxStreak() | Write the loop |
| Current streak | current_streak, from today | currentStreak(), from the newest note | Write it |
| This week against last | compare: true | Not in the docs | Write it |
| Calendar | A year of days | A month at a time | Write it, or add a plugin |
| What you install | Dashy | Tracker | Dataview, JavaScript queries on |
Tracker also draws line, bar, pie and bullet charts from tags, text and inline fields. Dashy reads frontmatter only. If you track things that live in the text of your notes, Tracker is the better fit.
Variations
Two habits on one grid
Each habit in its own colour. A day with both shows the first one listed. Guide.
```heatmap
source: Diary
layers:
- field: gym
color: orange
label: Gym
- field: read
color: green
label: Reading
```
Weekends off
Saturday and Sunday neither break the run nor add to it. Friday and Monday join up. Guide.
```stats
items:
- label: Workdays in a row
source: Diary
field: gym
agg: current_streak
days: weekdays
```
Holidays do not count
Mark a day with vacation: true. That day is skipped: it neither counts nor breaks the run.
```stats
items:
- label: Days in a row
source: Diary
field: gym
agg: current_streak
skip_field: vacation
```
A habit with a number
Steps instead of a tick. A day counts once it reaches the bar.
```stats
items:
- label: 8000+ steps, days in a row
source: Diary
field: steps
agg: current_streak
at_least: 8000
```
If a block shows an error
A block that cannot draw says why, inside the note. The usual three:
- The field is missing. No note in
Diaryhasgym. Check the spelling and the folder. - The field holds text.
gym: doneis text, not a checkbox. Set the property type to Checkbox. - No note has a date. A card with
periodsays so when no note is namedYYYY-MM-DD. If the date is a property, adddate_field: date.
Related guides

Friday joins up with Monday. Holidays do not end the run.

Each in its own colour, on one year of days.

A number summed by week, with a goal line.
Dashy is in Obsidian's community plugins. Search for Dashy, install, enable, then paste the note above.