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;
Screenshots
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.
Note format
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.
Installation
From GitHub (Emacs 29+)
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:
Manual
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/")
Usage
Global keybindings
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 |
Dashboard
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.3.5 | 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 |
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) |
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 |
Listing all tags and keywords
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 the keyword list, press R to rename a keyword across all notes. You'll be prompted for a new name, and every bold occurrence (*old*) will be replaced in all note files that contain it.
Tag exclusion
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 search syntax
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 search syntax
Keyword queries use SPC-separated terms with OR logic — any matching term is enough:
| Query | Meaning |
|---|---|
emacs |
Notes with emacs as a bold keyword |
emacs lisp |
Notes with either emacs or lisp |
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.
Search views
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 |
Stitched view
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 |
Capturing notes
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.
Private paragraphs
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
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)
Updating a note's timestamp
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.
Org dynamic blocks
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 |
Space-separated tags (OR logic) | — |
:keywords |
Space-separated keywords (OR logic) | — |
: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
Performance
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 first load reads all files; subsequent operations are fast. Use M-x tiles-clear-cache to force a full reload.
Customization
All settings are available via M-x customize-group RET tiles.
Variables
| 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-tag-mode |
Controls tag behavior (see below) | 'unrestricted |
Tag mode
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)
Faces (colors)
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")
Changelog
- 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.
Acknowledgements
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.
Disclaimer
This package was developed with the assistance of Claude, an AI assistant created by Anthropic.
License
GNU GPLv3