TILES is an Emacs package for taking quick, title-less notes.
Each note (or tile, if you will) is a single paragraph stored in its own org file, organized through tags and bold keywords rather than hierarchies. TILES tries to keep it simple: there are no dependencies (except for Emacs, version 27.1 at least), no links between notes (except the Org Mode syntax), no backlinks, no graphs, no database; every note is a paragraph in its own text file.
I created this package because I wanted a note taking system with the following features:
- focus on one paragraph (like Logseq): one paragraph = one note;
- offers a bird's-eye view (quick preview) of recent notes (similar to Howm);
- quick note preview, quick note edit;
- color coding depending on the note's age (sort of like Howm, but not really);
- title-less, to reduce friction (why having to stop the thought process to create a title that's never used afterwards?);
- can use the Dynamic Block features in Org Mode (like Denote and Denote Org, ideal if you want to use your notes to create other documents;
- can stitch notes together, after applying a search filter (like Howm, ideal if you want to use your notes to create other documents;
- uses tags for hierarchy but also uses bold keywords (extracted automatically from words that are marked as bold);
- can have follow-up text inside a note (and undertile, if you will), a kind of a meta-content (a private content inside a note), which is a paragraph prefixed with '&&' hidden everywhere (not exported with Dynamic Blocks actions) except expanded view in the dashboard and, of course, in note editing buffer;
- search after tags and/or keywords only (who really wants to search for anything else?);
- no external dependencies needed except at least version 27.1 of Emacs and Org Mode (built-in);
- uses Org Mode format for bold, italic, links, in-line footnotes;
Dashboard for TILES, default view:

Dashboard with Org Mode markup toggled on (notice the red & character in front of a note, meaning there's some meta content there

A regular note, expanded to reveal the keywoards:

A note with meta-content, expanded to reveal the meta-content (meta-content is not exported, nor visible with stitching):

An example of a regular note, no title, Org Mode markup, tags on the last line (mandatory by default but can be disabled, see tiles-tag-mode below):

An example of a note with meta-content, added after main content, prefixed with &&:

The result of stitching all notes sharing the same keyword ("Falcon 9" in this case):

If you're wondering about the font I'm using inside my Emacs, it's TX-02 Berkeley Mono 18 Medium Condensed.
Each note is a file named TYYYYMMDDHHMMSS.org (T followed by a timestamp up to seconds) and stored in a predefined folder:
The Mars Sample Return (*MSR*) mission involved
a collaboration between *NASA* and *ESA* to
retrieve samples collected by the *Perseverance*
rover[fn:: Launched in July 2020].
space/mars
- Content: paragraph(s) with full org-mode formatting (
*bold*,/italic/,[[links]], inline footnotes); - Optional private paragraphs prefixed with
&&(see Private paragraphs below); - Blank line separator after the content;
- Last non-empty line: tags separated by
/(always parsed as the tag line); tags are mandatory by default, but can be disabled; - Bold words (
*word*) double as searchable keywords (optional); - Multi-paragraph notes are supported but discouraged; the parser always takes the last non-empty line as tags.
Emacs 29 introduced package-vc-install, which can install packages directly from GitHub:
(package-vc-install "https://github.com/ctanas/tiles")Then add to your config:
(require 'tiles)Clone the repository and add to your load path:
(add-to-list 'load-path "/path/to/tiles")
(require 'tiles)Set your notes directory (default ~/notes/tiles/):
(setq tiles-directory "~/notes/tiles/")All commands are under the C-c m prefix:
| Key | Command | Description |
|---|---|---|
C-c m m |
tiles-show-notes |
Open dashboard |
C-c m n |
tiles-new |
Create a new note (buffer) |
C-c m q |
tiles-quick |
Quick capture via minibuffer |
C-c m y |
tiles-yank |
Quick capture from clipboard |
C-c m t |
tiles-tag-search |
Search by tags |
C-c m k |
tiles-keyword-search |
Search by keywords |
C-c m m launches the dashboard, which displays a chronological list of all notes. Each entry shows color-coded timestamps (showing hours and minutes to save space), inline previews, tags, and keywords. Timestamps are color-coded: green for today, darker green for recent (< 2 weeks), faded grey for older notes. The selection highlight is Lufthansa yellow. While the dashboard displays truncated timestamps for brevity, the actual filenames include timestamps down to the second level, allowing you to create multiple notes within the same minute without conflicts.
*T*agged *I*nstant *L*ightweight *E*macs *S*nippets (TILES), v0.5.2 | 42 notes | loaded in 0.023s
════════════════════════════════════════════════════════════════════════
[SPC] view, [RET] open, [TAB] expand, [f] format, [d] chg date, [u] touch, [0] stitch, [D] delete, [g] refresh, [+] more, [q] quit
[t] filter tag, [k] filter keyword, [F] exclude tags, [T] list tags, [K] list keywords, [c] clr search, [C] clr excl, [l] new tile
7 days until New Moon: Mon, 17 February 2026
──────────────────────────────────────────────────────────────────────
2026-02-06 08:12 Hello world, I'm the first tile! meta/test
2026-02-06 08:12 This note is ready for production meta/prod
Dashboard keybindings:
| Key | Action |
|---|---|
n/p |
Navigate notes |
SPC |
Open editable preview split (follows cursor) |
RET |
Open note file |
M-RET |
Open note file read-only |
TAB |
Toggle expanded view (private &&, keywords, stats) |
M-up |
Move selected note up |
M-down |
Move selected note down |
d |
Change note date/timestamp (renames file) |
u |
Touch (update timestamp to now) |
w |
Copy note content to the kill ring (verbatim, markup preserved) |
W |
Copy note content to the kill ring as plain text (markup stripped) |
D |
Delete note (with confirmation) |
t |
Filter displayed notes by tag |
k |
Filter displayed notes by keyword |
T |
List all tags |
K |
List all keywords |
F |
Exclude tags (hide notes with these tags) |
c |
Clear search filter (keeps exclusion) |
C |
Clear tag exclusion (keeps search filter) |
f |
Toggle raw preview (strip org formatting) |
+ |
Load next batch of notes |
0 |
Stitch displayed notes into flowing view |
l |
New note (same as C-c m n) |
g |
Refresh |
q |
Quit |
M-x tiles-list-tags (or T in the dashboard) displays all unique tags with occurrence counts, sorted alphabetically. M-x tiles-list-keywords (or K) does the same for bold keywords. In both buffers, items that appear in both sets are shown in bold. Press RET to filter the dashboard by the selected item.
Sorting: a sorts alphabetically (a-z), o sorts by occurrence (high to low), d toggles ascending/descending.
In either list, press R to rename the tag or keyword on the current line across all notes. You'll be prompted for a new name; for keywords every bold occurrence (*old*) is replaced, and for tags the entry is updated in every tag line that contains it. Hyphen normalization is respected on the keyword side, so *Falcon-9* is found when renaming "Falcon 9". Tag rename also respects tiles-tag-mode: in restricted mode the new name must be in the allowed list, and in required-one-of mode renaming a required tag away from the required list prompts for confirmation.
Press F in the dashboard to exclude notes by tag. Enter one or more space-separated tags and any note carrying those tags will be hidden. The exclusion filter works independently from the search filter (t/k): you can exclude some tags, then search within the remaining notes. c clears only the search filter (keeping the exclusion), while C clears only the exclusion (keeping the search filter). The dashboard title shows the active exclusion (e.g., | excluding: journal draft).
Tag queries use / for AND and SPC for OR:
| Query | Meaning |
|---|---|
b218/lx2026 |
Notes with both b218 and lx2026 |
b218 misc |
Notes with either b218 or misc |
b218/lx2026 misc |
(b218 AND lx2026) OR misc |
This syntax applies everywhere: tiles-tag-search, dashboard filter (t), and dynamic block :tags parameter.
Keyword queries use the same AND/OR grammar as tag search, plus ! for negation:
| Operator | Meaning |
|---|---|
| space | OR (any group matches) |
/ |
AND (all terms in the group must match) |
! (prefix) |
NOT (the keyword must not be present) |
| Query | Meaning |
|---|---|
emacs |
Notes with emacs as a bold keyword |
emacs lisp |
Notes with either emacs or lisp |
emacs/lisp |
Notes with both emacs and lisp |
emacs/!lisp |
Notes with emacs but not lisp |
emacs/lisp misc |
(emacs AND lisp) OR misc |
Keywords are the *bold* words extracted from note content. Hyphens in keywords are normalized to spaces for matching and display — *Falcon-9* and *Falcon 9* are treated as the same keyword ("Falcon 9") — but the note content itself is never modified. This syntax applies to tiles-keyword-search, dashboard filter (k), and dynamic block :keywords parameter.
Tag and keyword searches (C-c m t / C-c m k) open a two-panel view: results list on top, live preview below.
| Key | Action |
|---|---|
n/p |
Navigate results |
RET |
Open note file |
SPC |
Toggle to stitched view |
r |
Refine search (new query) |
t/k |
Switch to tag/keyword search |
q |
Quit |
Press SPC from the search view to enter the stitched view: all matching notes concatenated into a single flowing org buffer, stripped of tag lines and private (&&) paragraphs, in inverse chronological order. This is useful for reading related notes as continuous prose or if you want to include multiple related notes into another document, like a newsletter.
| Key | Action |
|---|---|
n/p |
Jump between note boundaries |
RET/e |
Open the source file at point (with focus mode) |
SPC |
Toggle back to two-panel view |
r |
Refine search |
q |
Quit |
C-c m n opens a capture buffer. Write your paragraph, add a blank line, then your tags. Press C-c C-c to save, C-c C-k to cancel. While keywords are not mandatory, tags are (by default), so if the user forgets to add tags, it will be asked to do so. The tag line (last line) is displayed in red using the tiles-tags face, matching the tag color in the dashboard.
Inside the capture buffer, C-c m t (tiles-insert-tag) inserts a tag at point using minibuffer completion:
- Unrestricted mode: suggests all tags found in existing notes (free input allowed).
- Restricted mode: completes from the allowed list with
require-matchenforced. - Required-one-of mode: suggests the required tags (free input still allowed).
- Inhibit mode: displays a message explaining that tags are disabled and how to re-enable them.
Outside a capture buffer, C-c m t continues to run tiles-tag-search as usual.
For faster capture, C-c m q prompts for content and tags directly in the minibuffer. C-c m y does the same but pre-fills the content from the clipboard (kill ring), which you can edit before confirming.
Any paragraph in a note that starts with && is treated as private. Private paragraphs are hidden from dashboard previews, stitched views, search panels, and dynamic blocks. They are only visible in two places: when expanding a note with TAB in the dashboard, and when editing the file directly.
This is useful for keeping personal annotations, reminders, or context that you don't want surfacing in exports or shared views.
The Mars Sample Return (*MSR*) mission involved
a collaboration between *NASA* and *ESA*.
&& Personal note: double-check the timeline
with the ESA press release from January.
space/mars
In the example above, the && paragraph will not appear in previews or stitched output, but pressing TAB on this note in the dashboard will reveal it in the expanded area.
When formatted preview is on (i.e., tiles-preview-raw is nil), notes containing private paragraphs display a red & indicator right before the preview text in the dashboard, so you can tell at a glance which notes have hidden content.
Focus mode centers the buffer content with approximately 80-character line width (using window margins, similar to olivetti-mode) and adds visual padding at the top. No hyphens or hard wraps — just soft word wrap via visual-line-mode. The padding is purely visual and is never saved to the file.
Focus mode is enabled by default when creating new notes. You can also toggle it manually with M-x tiles-focus-mode in any capture buffer.
To disable focus mode by default:
(setq tiles-focus-default nil)Open a note read-only with M-RET from the dashboard. In a read-only tile buffer:
| Key | Action |
|---|---|
m |
Toggle a list of the most similar notes at end of buffer |
q |
Close the read-only buffer |
Similarity is scored with TF-IDF over shared content tokens. Tokens that are *bold* in the current note (i.e. its keywords) get a tiles-similar-keyword-weight boost (default 3.0), so they dominate ranking — but plain content overlap is enough to surface a match on its own. Common words self-downweight via IDF; no stopword list is needed.
The number of results shown is controlled by tiles-similar-count (default 5). Press m again to hide the block. Each result is rendered as its content followed by a blank line; the original note's tag line keeps its red color, the appended candidates do not.
While editing a note, M-x tiles-touch updates the file's timestamp to the current time and renames the file accordingly. Asks for confirmation before proceeding. Useful for bumping a note to the top of the chronological list after editing.
TILES provides two dynamic block types for embedding note data in org files:
tiles-notes - Insert a linked list of matching notes:
#+BEGIN: tiles-notes :tags "space mars" :sort "newest" :limit 10
- [[file:~/notes/tiles/T20260206081250.org][2026-02-06 08:12:50]] The Mars Sample Return... space/mars
- [[file:~/notes/tiles/T20260206081227.org][2026-02-06 08:12:27]] NASA announced today... space/nasa
#+END:
tiles-files - Embed note contents directly:
#+BEGIN: tiles-files :tags "journal" :keywords "review" :separator "\n-----\n"
First matching note content...
-----
Second matching note content...
#+END:
Parameters (all optional):
| Parameter | Description | Default |
|---|---|---|
:tags |
Tag query (space=OR, /=AND) |
— |
:keywords |
Keyword query (space=OR, /=AND, !=NOT) |
— |
:sort |
"newest" or "oldest" |
"newest" |
:limit |
Maximum number of notes | unlimited |
:separator |
String between notes (tiles-files only) |
blank line |
C-c C-x xto insert a dynamic block from a menuC-c C-x C-uto update the block under cursor
TILES uses an in-memory cache that stores parsed note data keyed by filepath. Files are only re-read from disk when their modification time changes. The cache is persisted to tiles-cache-file (default: ~/.emacs.d/tiles-cache.el) between sessions, so warmup is a one-time cost; entries are still revalidated against file mtime on access, so a stale cache file is always safe. Set tiles-cache-file to nil to disable persistence.
A reverse index (tag → files, keyword → files) is populated alongside the parsed-note cache, so tag/keyword search, dashboard filtering, and the tag/keyword lists run in O(matching files) rather than O(all files).
When tiles-watch-files is non-nil (default), TILES uses Emacs' built-in file-notify to watch tiles-directory for external changes (another Emacs instance, mobile sync, git pull, etc.) and invalidates affected cache and index entries automatically.
Use M-x tiles-clear-cache to force a full reload (also wipes the persistent cache file).
All settings are available via M-x customize-group RET tiles.
| Variable | Description | Default |
|---|---|---|
tiles-directory |
Root directory for notes (recursive) | ~/notes/tiles/ |
tiles-preview-length |
Max characters for inline preview | 105 |
tiles-line-padding |
Extra padding beyond preview and tags | 22 |
tiles-preview-raw |
Strip all org formatting from previews | t |
tiles-dashboard-limit |
Max notes per page (nil = unlimited) |
50 |
tiles-focus-default |
Enable focus mode for new notes | t |
tiles-fancy-separators |
Use Unicode box-drawing separators (═/─) | t |
tiles-year-subfolders |
Save new notes in a YYYY/ subfolder |
nil |
tiles-cache-file |
Path for the persistent cache (nil to disable) |
~/.emacs.d/tiles-cache.el |
tiles-watch-files |
Watch tiles-directory via file-notify |
t |
tiles-tag-mode |
Controls tag behavior (see below) | 'unrestricted |
tiles-similar-count |
Number of notes shown by tiles-show-similar |
5 |
tiles-similar-keyword-weight |
TF-IDF boost for shared bold keywords | 3.0 |
tiles-similar-min-token-length |
Min token length for similarity scoring | 3 |
tiles-dblock-html-anchor |
Emit @@html:<a id="...">@@ anchor before each note in tiles-files dblocks |
nil |
tiles-dblock-no-anchor-tags |
Tags that suppress the HTML anchor when it is enabled | nil |
tiles-tag-mode controls how tags work across the entire package. It accepts four kinds of values:
'unrestricted (default) — tags work as described throughout this document: any string is accepted, search and filter are fully available.
'inhibit — tags are completely disabled. Capture (both buffer and quick) does not prompt for tags; notes are saved with an internal placeholder. Tag-based search (C-c m t), dashboard tag filter (t), tag exclusion (F), and tag listing (T) are all disabled and will show an error if invoked. Tags are not displayed in the dashboard, and the second keybinding help line is simplified to omit tag-related keys.
A list of strings — only those tags are accepted. Tag prompts use completing-read with the list as candidates and require-match enforced, so any completion framework (Vertico, Ivy, Helm, etc.) will show the candidates automatically. The first element of the list is used as the default when the user provides no input. Tags entered manually in the capture buffer are validated against the list at save time, and the save is rejected if any tag is not in the list. Dashboard tag filter (t), exclusion (F), and search (C-c m t) also use completion-based prompts restricted to the allowed list.
(required-one-of TAG...) — any tag is accepted, but at least one from the list must be present. Prompts suggest the required tags via completing-read with free input still allowed, so completion frameworks will surface the candidates without enforcing them. If the saved note contains no tag from the required list, the save is rejected with a clear error message. Dashboard filter, exclusion, and search prompts also suggest the required tags but accept free input.
Case 1 — Unrestricted (default). No configuration needed; this is the default. To restore it explicitly:
(setq tiles-tag-mode 'unrestricted)Case 2 — Inhibit tags. Use this if you prefer a pure keyword-based workflow with no tagging at all:
(setq tiles-tag-mode 'inhibit)With this setting: capture never asks for tags, the dashboard hides tag columns and tag-related keybindings, and t, F, T, and C-c m t all report an error.
Case 3 — Restricted tag list. Define an explicit vocabulary; the first element becomes the default when the user confirms without typing:
(setq tiles-tag-mode '("work" "personal" "journal" "idea" "reference"))With this setting: C-c m n and C-c m q/C-c m y prompt for a tag using completion restricted to the list (work is offered as the default); t, F, and C-c m t in the dashboard also use completion. Any tag typed manually in the capture buffer that is not in the list will be rejected at save time with a clear error message.
Case 4 — At least one required tag. Allow any tags, but enforce that at least one comes from a required set:
(setq tiles-tag-mode '(required-one-of "work" "personal" "journal"))With this setting: C-c m n and C-c m q/C-c m y prompt for tags with the required tags suggested via completion (free input still allowed); t, F, and C-c m t in the dashboard also suggest the required tags but accept any string. If the saved note contains none of the required tags, the save is rejected with a clear error message.
Example configuration:
(setq tiles-directory "~/Documents/tiles/")
(setq tiles-preview-length 120)All faces can be customized via M-x customize-face or in your config:
| Face | Description | Default |
|---|---|---|
tiles-timestamp-today |
Today's timestamps | #228b22 |
tiles-timestamp-recent |
Recent timestamps (< 2 weeks) | #3a5a2a |
tiles-timestamp-old |
Older timestamps (> 2 weeks) | #999999 |
tiles-tags |
Tag display | #a00000 |
tiles-keywords |
Keyword display in expanded view | #006600 |
tiles-notes-hl-line |
Selection highlight | #FFC700 |
tiles-notes-expanded |
Background of expanded lines | #FFF8DC |
Example:
(set-face-attribute 'tiles-timestamp-today nil :foreground "#008800")
(set-face-attribute 'tiles-notes-hl-line nil :background "#FFD700")- 0.5.2 — Keyword search now supports AND (
/) and NOT (!) alongside the existing space-as-OR (emacs/!lisp=emacsAND NOTlisp). Tag rename:Rin the tag list renames a tag across every note's tag line, parallel to keyword rename. New dashboard keywcopies the selected note (private&¶graphs stripped) to the kill ring. The HTML anchor thattiles-filesdynamic blocks used to emit unconditionally is now opt-in viatiles-dblock-html-anchor, with per-tag suppression viatiles-dblock-no-anchor-tags. Polish:grefresh in the dashboard preserves the previously selected note's position;tiles--strip-org-markupalso handles underline/strikethrough/code/verbatim; touch logic deduplicated;tiles-stitched-prevearly-breaks;tiles-clear-cachealso resets the persistent-cache load flag. - 0.5.1 — Similar notes: press
min a read-only tile buffer (M-RETfrom the dashboard) to list the toptiles-similar-countnotes most similar to it. Scoring is TF-IDF over shared content tokens, with bold*keywords*boosted bytiles-similar-keyword-weight.qcloses the read-only buffer. - 0.5 — Persistent on-disk cache (
tiles-cache-file, mtime-validated, disable by setting tonil) so warmup is a one-time cost across sessions. Reverse index (tag → files, keyword → files) speeds up tag/keyword search, dashboard filter, and the tag/keyword lists from O(all files) to O(matching files).file-notifywatches (tiles-watch-files, defaultt) catch external edits (other Emacs instances, mobile sync,git pull). Newtiles-year-subfoldersoption to save new notes in aYYYY/subfolder (loading remains recursive, so existing notes in any layout are still found). Focus-mode fixes: cursor positioning now uses the header line for top padding (no more stray padding lines saved to file), and the focus-mode overlay no longer duplicates when revisiting a note. - 0.4 — MELPA-compliance pass: removed load-time
global-set-key(bindings now applied via a keymap), addressedpackage-lint,checkdoc, and byte-compile warnings. Newtiles-insert-tagcommand (C-c m tinside a capture buffer) inserts a tag at point using completion driven bytiles-tag-mode. Newrequired-one-oftag mode ((required-one-of TAG...)): any tags are accepted, but at least one from the list must be present. Fix: focus mode no longer leaves stale state on save. - 0.3.5 — Tag mode control via
tiles-tag-mode:'unrestricted(default),'inhibit(tags disabled, tag search/filter suppressed), a list of allowed tag strings (completion-based prompts, first element is the default), or(required-one-of TAG...)(any tags accepted, but at least one from the list must be present). Fix: deleting the last note now correctly refreshes the dashboard to an empty state instead of leaving the deleted note visible. - 0.3.4 — Keyword rename:
Rin the keyword list renames a keyword across all note files. - 0.3.3 — Unicode box-drawing dashboard separators (
tiles-fancy-separators, set tonilfor ASCII fallback). Tag line shown in red (tiles-tagsface) when editing notes. Focus mode when opening notes from stitched view (RET). - 0.3.2 — Tag exclusion filter (
Fto exclude,Cto clear, independent from search filter). Focus mode for distraction-free editing (enabled by default,tiles-focus-default). Interactive tag/keyword lists with occurrence counts and sorting (o/a/d). Keyword hyphen normalization. Dashboard keybindings:Tlist tags,Klist keywords,utouch. Stitch confirmation when no filter is active. - 0.3.1 — Red
&indicator in formatted preview for notes with private paragraphs. Newtiles-list-tagsandtiles-list-keywordscommands to browse all unique tags/keywords (with bold cross-highlighting). - 0.3 — Private paragraphs: paragraphs starting with
&&are hidden from dashboard previews, stitched views, search panels, and dynamic blocks. Only visible viaTABexpansion in the dashboard or direct file editing. - 0.2 — Initial public release.
Many thanks to Protesilaos Stavrou for Denote and Denote Org, Kazuyuki Hiraoka for Howm, Andrei Sukhovskii for Howm Manual, Jethro Kuan for Org-roam, Jason Blevins for Deft, Zachary Schneirov for Notational Velocity, and to all the developers of Logseq and Obsidian for their inspiration into creating this package.
This package was developed with the assistance of Claude, an AI assistant created by Anthropic.
GNU GPLv3