Habbiter

unlisted

by Nourix

Habit and tracker grids that live inside a note — a month calendar of checkboxes, or any grid of rows and columns you name yourself.

Updated 20d agoGPL-3.0
View on GitHub

Habbiter

Habit trackers and custom grids, directly inside your Obsidian notes.

Obsidian Latest Release License CI

Habbiter turns a fenced code block into an interactive habit tracker that stays part of your Markdown note. Use a calendar for daily habits or build a custom grid of checkboxes and counters—without adding a separate dashboard or changing the look of your vault.

فارسی · Install · Examples · Development

Install · Examples · Features · Development

See it in action

Habbiter promotional banner showing calendars, Persian RTL support, and wrapping grids

Habbiter in the Obsidian light theme, showing a month calendar, Persian RTL tracker, and wrapping grid Habbiter in the Obsidian dark theme, showing a month calendar, Persian RTL tracker, and wrapping grid

Habbiter wrapping grid layout

The gallery uses the real tracker renderer from the preview harness. Persian UI text is rendered with Vazirmatn.


Install

Until it is in the community list, copy main.js, manifest.json and styles.css from this repository into <vault>/.obsidian/plugins/habbiter/, then enable it in Settings → Community plugins. The built main.js is committed, so there is nothing to build first.

Making one

Right-click in the editor → Insert habit tracker…, or run Habbiter: Insert tracker from the command palette.

Either opens a form with a live preview of the tracker above it — the real component, not a picture of one, so you can click around in it before you commit. The preview's clicks are thrown away.

The form writes a fenced block into the note, which is the only thing that ends up in your markdown:

```habbiter
id: hb-m8x2q1a4
title: Meditate
```

To change one later, hover it and use ⋯ → Edit tracker… — the same form, opened on what is already there. Renaming a row or a column in the form keeps its ticks; the form matches the old list against the new one by position.

What the block can say

Everything is optional except id. Anything left out follows Settings → Habbiter, so a block only ever states what you chose.

KeyWhat it does
idWhere the ticks are filed. Written for you; do not reuse one.
titleThe tracker's name. A month tracker with no title is named by its month.
modemonth — the month as a calendar. grid — rows and columns.
cellcheck for a checkbox, count for a counter.
goalCounter only: what a full cell means. Clicking cycles up to it and back to zero.
rowsGrid only: a list of row names.
columnsGrid only: a list of names, a plain number, or days for the month.
bandDay columns: wrap runs the month on to the next line, week breaks it at the weeks, none keeps one scrolling line.
calendargregorian or persian.
monthcurrent, or a year-month in the block's own calendar: 2026-09.
weekStartauto, a weekday name, or 0–6 with 0 as Sunday.
localeA tag like fa or en-GB for month and weekday names. Empty follows the app.
numeralslocale or latin.
dayNumbers, weekdays, totals, streakWhat the tracker shows.
sizeThe side of a cell in pixels, 20 to 44.
wrapnone for a band of its own, or start / end to let text run beside it. left and right are accepted and mean the same.
trackersA list, to put several in one block. See below.
layoutSeveral in one block: row side by side, or deck stacked with one in front.

A month that fits

A grid with a column per day is 31 wide, which no note is — flat, it ends in a horizontal scrollbar, and a scrollbar is where a tracker stops being glanced at. So the month runs on to the next line instead, taking whatever width the note has:

Meditate   Total 18   Streak 3
 M  T  W  T  F  S  S  M  T  W  T  F  S  S  M  T  W  T
 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
 ■  ■  □  ■  □  ■  ■  □  ■  ■  □  ■  □  ■  ■  □  ■  ■
19 20 21 22 23 24 25 26 27 28 29 30
 □  ■  ■  □  ■  □  ■  ■  □  ■  ■  □

Each day carries its own date, in the box that wraps with it — a row of dates above a row of cells would be two things wrapping separately, and they would stop corresponding at the first line break.

band: week breaks the month at the weeks instead, which lines the columns up down the page and lets the weekday names be written once. It is worth having for several habits at once, where the calendar cannot help; for one habit it is another way of drawing the calendar. band: none keeps the single scrolling line.

A grid of four habits across the month:

```habbiter
id: hb-k1p7w2
title: Morning routine
mode: grid
rows:
  - Meditate
  - Read
  - Walk
columns: days
totals: true
```

A week of counters:

```habbiter
id: hb-r4t9m0
title: Reps
mode: grid
cell: count
goal: 4
rows: [Push-ups, Squats, Plank]
columns: [Mon, Tue, Wed, Thu, Fri]
```

Writing beside one

A block can carry text as well as a tracker, and the two sit side by side:

```habbiter
id: hb-n3f8
title: Meditate
size: 22
side: start
text: |
  Ten minutes, **before** coffee.

  See [[Morning routine]] for the rest of it.
```

The text is ordinary markdown, rendered by Obsidian — links, formatting and embeds all work, set in the note's own font at the note's own size. side: start puts the tracker where the line starts (the left in English, the right in Persian) and side: end puts it the other way round.

Beside means beside, at every width. The tracker's grid has a size it cannot go under, so the text column is the part that gives: narrow the pane and the text gets a narrower column rather than dropping underneath.

The easy way to write it is the builder: ⋯ → Edit tracker… has a Text beside it box with the live preview above, and ⋯ on the tracker swaps the sides without opening anything.

Why the text lives in the block. The obvious way to do this is to float the block so the note's own paragraphs run past it, and that does not survive Live Preview: a floated block leaves the flow of the CodeMirror line holding it, the line measures as empty, and the editor comes apart. Inside the block's own container there is no editor to come apart, so what you see in the editor, in reading view, in an export and on a phone is the same thing. It was tried the other way round in 1.2.0 and taken out again.

For a tracker positioned freely on a surface rather than set in a document, Obsidian's canvas is the place, and a tracker works in a canvas card like anything else.

Several side by side

One block can hold a row of trackers. They lay themselves out across the width of the note and wrap, each one as wide as its own grid:

```habbiter
calendar: persian
size: 22
trackers:
  - id: hb-k2p9
    title: مدیتیشن
  - id: hb-m4x1
    title: مطالعه
  - id: hb-q7v3
    title: پیاده‌روی
```

Anything the row shares is said once at the top and inherited; anything one tracker does differently sits on its own entry. Each keeps its own id, so each keeps its own ticks.

The quickest way to build one is ⋯ → Add one beside this… on a tracker you already have. ⋯ → Take out of the row removes one again — its ticks are filed under its id, not its position, so they are still there if you put it back.

Reordering. ⋯ → Move earlier / Move later on any tracker in the row. The order is written into the block, which is the only place a markdown file can keep one — so it survives closing the note, syncing, and anyone else opening it. A pointer drag was tried and taken out again: a tracker in Live Preview lives inside CodeMirror, which has its own claim on pointer events, and a gesture that works in a browser and not in the editor is worse than no gesture at all.

Stacked, one in front

The other way a block can hold several: as a deck, with one card in front and the rest behind it.

⋯ → Add one stacked behind this… on a tracker you already have. That one click does both things — it asks for the new tracker and it stacks the block. On a block that already holds a row, ⋯ → Stack them, one in front puts them into a deck without adding anything, and ⋯ → Lay them out side by side puts them back.

Written out, it is one key:

```habbiter
calendar: persian
layout: deck
trackers:
  - id: hb-k2p9
    title: مدیتیشن
  - id: hb-m4x1
    title: مطالعه
  - id: hb-q7v3
    title: پیاده‌روی
```

Cards in a deck share one space, so every card is as big as the biggest of them. That means the deck draws every tracker at one cell size — the largest any of them asks for — so a stack of calendars comes out as a stack of matching cards rather than a small one adrift in a box built for a large one. Each tracker keeps its own size in the block, so it is its own size again in a row, in the builder, and if it is taken back out of the stack.

Swipe across the front card to bring the next one up. The swipe is never the only way: the arrows under the deck, the dots between them, and ← → on the dots all do the same job, because a pointer gesture inside CodeMirror is not something to bet the feature on. A tap stays a tap — a drag only starts once the pointer has travelled far enough, and further across than down, so ticking a box never turns into a swipe by accident.

The stack is a ring. Past the last card is the first one, before the first is the last, and neither arrow is ever the one that does nothing — so getting to the card behind the one in front is one press backwards, not a walk all the way round. A card that leaves the front sinks to the bottom of the stack rather than off the side of the deck.

⋯ → Move earlier / Move later moves a card through the stack, and ⋯ → Take out of the stack removes one. Only the front card takes clicks or Tab; the ones behind it are inert until they come forward.

What is not on offer: dragging a tracker to an arbitrary spot on the page. A note is a markdown document, not a canvas: there is nowhere in it to record that something sits 240px from the left, and anything faking it would come apart in reading view, in an export and on a phone. Obsidian's own canvas is the place for free positioning — a tracker works in a canvas card like anything else.

Using one

ClickTick, or count up one and wrap back to zero at the goal
Shift-clickCount back down
ArrowsMove around the grid
Space, EnterSame as a click
+, -Count up or down, past the goal if you want
Backspace, DeleteClear the cell
TabInto and out of the grid in one press

Every cell is a real button with a label, so a screen reader reads "Meditate — Monday 21 September 2026" and whether it is ticked. Today is marked with a ring as well as a colour, and a ticked cell is a solid block where an empty one is an outline — nothing here is carried by colour alone.

Persian

Set Calendar to Persian — in Settings for the whole vault, in the builder for one tracker, or from ⋯ → Show in the Persian calendar on a tracker in front of you. The months become Farvardin to Esfand, cut at the right days, named in Persian, counted in ۰–۹, and the week starts on Saturday. The calendar brings its own language: Language only needs setting to read a Persian calendar in some other one, and Numerals to count it in 0–9. A tracker in a right-to-left note lays itself out right to left.

Ticks are always filed under the real date, so switching a tracker between calendars re-labels the grid without losing a single one.

In a callout

A tracker works inside a callout, a quote, or a nested one, and editing it there writes back correctly — the block keeps the > on every line, including the blank ones, so the callout does not end where the tracker starts.

> [!tip] Morning
>
> ```habbiter
> id: hb-abc
> title: Meditate
> ```

This is worth saying because it was broken until 1.4.0. Obsidian's section info points at the section a block belongs to, and inside a callout that is the callout, not the fence — so an edit looked for a fence where > [!tip] was standing, found none, and refused to save.

Where the ticks are kept

In the plugin's own data file — .obsidian/plugins/habbiter/data.json — under each tracker's id. Obsidian Sync carries it like any other plugin state.

They are deliberately not written back into the note. Saving a value into the fence means editing the file your cursor is sitting in, and the cost of that lands exactly when the plugin is supposed to be invisible: a caret that jumps mid-sentence, a sync conflict between two devices that both ticked today.

The practical consequences are worth knowing. Copying a block to another note copies its id, so both places show the same tracker — give the copy a new id if you wanted a separate one. Deleting a note leaves its ticks in data.json. And a tracker in an exported or read-only note still draws and still takes clicks, but says out loud that they are not being kept.

Theming

Everything the plugin paints comes from a variable, and every variable falls back through your theme's token, then Obsidian's, then a literal. A snippet can move any of them:

.hb-root {
  --hb-accent: var(--color-green);
  --hb-radius: 50%;
  --hb-cell: 32px;
}

--hb-page is the one worth knowing about. A tracker wider than the note scrolls sideways under a row-label column that stays put, and the label has to be opaque or the cells slide through it. CSS cannot read the colour actually painted behind an element, so --hb-page is what the label sits on, and it defaults to the page. Set it where a tracker lives on something else — Bookcloth does this for callouts, embeds, sidebars and popovers.

Building

npm install
npm run build     # typecheck, test, then bundle to main.js
npm test          # the date arithmetic and the block format
npm run dev       # rebuild on change

The tests cover the two things that are expensive to get wrong: the calendar maths behind the grids, including the Persian leap years, and the block format, which is written back into somebody's note.

preview/ renders the real component in a browser against a stand-in for the obsidian module, so a change to the renderer or the stylesheet can be checked in both themes without a vault. See preview/README.md.

Releasing

Obsidian looks a plugin's version up by its release tag, so the tag is the version and carries no v:

# bump manifest.json, package.json and versions.json together, then
npm run build && git commit -am "1.0.1"
git tag -a 1.0.1 -m "Habbiter 1.0.1"
git push origin main 1.0.1

Pushing the tag runs .github/workflows/release.yml, which refuses to build if the tag and manifest.json disagree, then publishes a release carrying main.js, manifest.json and styles.css — downloadable from the tag, which is the whole point of building them.

The same workflow can also be started from the Actions tab with a version, and will mint the tag itself from the branch it was run on — the path for anywhere a tag cannot be pushed by hand. It refuses a version the manifest does not claim, and refuses to overwrite a release that is already out with its files. A release that did not finish — unpublished, or published with nothing attached — it repairs and completes, and it asserts at the end that all three files really are on the tag.

.github/workflows/ci.yml runs on every push: typecheck, tests, bundle, and a check that the committed main.js is the one the source builds.

License

GNU General Public License v3.0.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.