org-timegrid is a beautiful SVG week calendar for Org mode. It reads timed active
timestamps from your Org files and writes edits back to their source headings.
Create, move, resize, copy, rename, and remove calendar blocks with the mouse or keyboard. The same renderer can add a compact, read-only day view to an existing Org Agenda.
org-timegrid-demo.mp4
The palette comes from the theme, and is redrawn when the theme changes.
![]() |
![]() |
Agenda view:
![]() |
![]() |
You need Emacs 29.1 or later with SVG support and Org 9.6 or later. Check SVG
support with (image-type-available-p 'svg). There are no other dependencies.
With Elpaca:
(use-package org-timegrid
:ensure (:host github :repo "Gleek/org-timegrid")
:commands (org-timegrid-week)
:bind ("C-c c" . org-timegrid-week))For a manual install, put the .el files on load-path, evaluate (require 'org-timegrid-org), then run M-x org-timegrid-week.
The following is a self-contained Elpaca configuration that can be pasted into
your Emacs configuration. It reads events from your existing
org-agenda-files, writes entries created in the calendar to calendar.org
inside org-directory, and enables the optional day strip in Org Agenda.
(require 'org)
(use-package org-timegrid
:ensure (:host github :repo "Gleek/org-timegrid")
:demand t
:bind (("C-c a c" . org-timegrid-week))
:init
;; The symbol `agenda' means: read events from `org-agenda-files'.
(setq org-timegrid-org-files 'agenda
;; Recommended compact scale; use 1.0 to match Emacs's default font.
org-timegrid-default-zoom 0.7
org-timegrid-org-capture-file
(expand-file-name "calendar.org" org-directory)
org-timegrid-org-capture-template
'(:target file
:template "* %{title}\n%{time-range}\n%?")
;; Save Org buffers immediately after edits made in the calendar.
org-timegrid-org-auto-save t
;; Set this to nil if repeating entries should be hidden.
org-timegrid-org-show-repeaters t
;; The first matching tag supplies an event's colour.
org-timegrid-org-tag-color-alist
'(("work" . indigo)
("personal" . green)
("reading" . yellow)
("errand" . cyan)))
:config
(require 'org-timegrid-org))
;; This is part of the same package. It adds a read-only day strip to Org
;; Agenda; pressing RET on the strip opens the editable week view.
(use-package org-timegrid-agenda
:ensure nil
:after org-agenda
:demand t
:init
;; nil inserts the strip at the top. To place it after a particular custom
;; agenda block, use a regexp matching that block's heading instead.
(setq org-timegrid-agenda-insert-after nil
org-timegrid-agenda-separator t
org-timegrid-agenda-minutes-before 180
org-timegrid-agenda-minutes-after 180)
:config
(org-timegrid-agenda-mode 1))Change org-timegrid-org-capture-file if new entries should go somewhere
else. The file does not need to be in org-agenda-files; org-timegrid always
queries its capture file as well. If you do not want the Org Agenda strip,
omit the second use-package form.
The Org backend reads active timestamps with a start time. They may be plain
timestamps or values attached to SCHEDULED and DEADLINE.
* TODO Design review :work:
<2026-08-24 Mon 14:00-15:30>
* TODO Standup
SCHEDULED: <2026-08-24 Mon 09:00-09:15>
* Call the vendor
<2026-08-24 Mon 15:00>
The last heading appears even though it has no TODO keyword. A timed active
timestamp is enough to put a heading on the calendar. A timestamp without an
end uses org-timegrid-default-duration-minutes, which defaults to 30 minutes.
Inactive timestamps do not appear on the time grid. A separate date-only rail appears for active timestamp entries with no day times
Repeaters are expanded into dated occurrences for display. Moving or resizing one occurrence edits the series anchor and preserves its repeater and warning modifiers. Edits leave source buffers modified but unsaved, like Org Agenda commands.
Set org-timegrid-org-show-repeaters to nil to omit repeating timestamps and
all of their occurrences.
Run M-x org-timegrid-week to open the week containing today.
| Gesture | Action |
|---|---|
| Drag empty space | Create an entry for that range |
| Drag a block | Move it |
| Drag a block's top or bottom edge | Resize it |
| super-drag a block | Duplicate it as an independent entry |
| shift-drag a block | Add another time to its source entry |
| Click | Put the cursor there |
| Double-click a block | Visit its Org heading |
| Wheel | Scroll the day |
Mouse edits snap to 15 minutes and use a 15-minute minimum. Release commits a
drag. Press C-g before releasing to cancel it.
The calendar has one cursor (point). A block is selected when the cursor sits on the slot where that block begins. This keeps keyboard and mouse selection in sync after redraws.
| Key | Action |
|---|---|
C-n / C-p, up/down |
Move the cursor by 15 minutes (configurable) |
C-f / C-b, left/right |
Move through overlapping lanes, then days |
C-v / M-v, SPC |
Move by one screen |
C-a / C-e |
Go to the first or last slot of the day |
C-l |
Center the view on the cursor |
C-x + / C-x C-- / C-x C-0 |
Zoom in, zoom out, or reset zoom |
n / p |
Select the next or previous block |
RET |
Visit the selected block, or create one at the cursor |
C-g |
Hide the cursor / Cancel unsaved new block |
M-down / M-up |
Move the selected block by 15 minutes |
M-right / M-left |
Move the block by one day |
S-down / S-up |
Move the end time |
C-S-up / C-S-down |
Move the start time |
t |
Enter a new time or range |
e |
Rename the heading |
d, backspace |
Remove its time, then optionally delete the heading |
M-w, then C-y |
Duplicate the block as an independent entry |
C-w, then C-y |
Move the block by cutting and restoring its timestamp |
M-w, then C-u C-y |
Add the pasted timestamp to the existing entry instead of duplicating it |
C-/, C-x u |
Undo the last calendar edit |
:, C-c C-q |
Change the selected entry's tags |
C-c C-t |
Change the selected entry's TODO state |
,, C-c , |
Set the selected entry's priority |
i |
Clock in to the selected entry |
O |
Clock out |
z, C-c C-z |
Add a note to the selected entry |
C-c C-x e |
Set the selected entry's effort |
C-c C-w |
Refile the selected entry |
$ |
Archive the selected entry |
b / f, M-b / M-f |
Shift by a day or week |
j / . |
Jump to a date or today |
g |
Reload the week from the backend |
q |
Quit |
C-s, C-r |
Isearch like forward or backward search. RET keeps the current match, C-g resets |
The Org backend can wrap other interactive commands for use in the calendar.
org-timegrid-org-command runs a command at the selected source heading.
org-timegrid-org-agenda-command finds the corresponding line in a live Org
Agenda buffer and runs the command there. Both wrappers return to the calendar
and reload it without resetting the cursor or scroll position.
(keymap-set org-timegrid-mode-map "i"
(org-timegrid-org-command #'org-clock-in))
(keymap-set org-timegrid-mode-map ","
(org-timegrid-org-agenda-command #'org-agenda-priority))The Agenda wrapper signals a user error when no live Agenda buffer contains the selected entry. Prefer the Org wrapper when the underlying Org command is available, since it does not depend on an Agenda view being open.
Enable the optional integration after loading org-timegrid-agenda:
(require 'org-timegrid-agenda)
(org-timegrid-agenda-mode 1)Org continues to build the agenda buffer. A finalize hook inserts a read-only
SVG view of the hours around the current time. Press RET on the image to open
the editable week view.
(setq org-timegrid-agenda-insert-after "To Refile"
org-timegrid-agenda-separator nil)The first option matches the agenda block above the strip. A nil separator lets the following block read as the untimed part of the same day section. The strip shades an edge only when an event continues beyond the visible window.
org-timegrid-day-blocks gets and lays out one day's blocks.
org-timegrid-day-image draws a chosen minute range as an SVG image. Neither
function needs a calendar buffer.
The standard Calendar opened by org-read-date shows dates containing events
in bold coloured text. The Org backend enables this by default for the j
date prompt. Other backends can reuse the integration directly:
(require 'org-timegrid-calendar)
(org-timegrid-calendar-read-date backend t nil nil "Time" default-time)Calendar still owns its buffer, navigation, and selection; the integration
only adds standard temporary Calendar marks while the prompt is active. Set
org-timegrid-calendar-highlight-dates to nil to disable them. Customize
org-timegrid-calendar-event-face to change their appearance, or set
org-timegrid-calendar-event-predicate to choose which backend events count.
The grid, text, cursor, and backgrounds use the current Emacs theme. Images redraw after a theme change and respect buffer-local face remapping.
Map Org tags to the built-in macOS-style palette or any colour string Emacs accepts:
(setq org-timegrid-org-tag-color-alist
'(("work" . indigo)
("business" . lime)
("reading" . green)
("errand" . cyan)
("urgent" . "#b4d74a")))The named colours are blue, cyan, teal, indigo, purple, pink,
red, orange, yellow, lime, green, brown, and graphite. The first
mapped tag wins. Set org-timegrid-org-color-function to colour by TODO state,
priority, property, or file.
| Variable | Default | Meaning |
|---|---|---|
org-timegrid-days |
7 |
Consecutive days shown in the main view |
org-timegrid-start-hour / org-timegrid-end-hour |
0 / 24 |
Hours drawn |
org-timegrid-pixels-per-minute |
0.9 |
Week-view scale |
org-timegrid-default-zoom |
0.7 |
Initial zoom; 1.0 matches Emacs's default font size |
org-timegrid-slot-minutes |
15 |
Cursor and edit granularity |
org-timegrid-cursor-step-minutes |
15 |
Normal keyboard step |
org-timegrid-keyboard-commit-delay |
0.25 |
Idle delay before a moved block is saved |
org-timegrid-default-duration-minutes |
30 |
Length used when no end is present |
org-timegrid-block-gap |
1 |
Gap between blocks, in pixels |
org-timegrid-corner-radius |
0 |
Block corner radius |
org-timegrid-nesting-indent |
8 |
Indent for contained events |
org-timegrid-title-clearance |
18 |
Space a parent keeps for its title |
org-timegrid-data-refresh-seconds |
300 |
Backend reload interval |
| Agenda variable | Default | Meaning |
|---|---|---|
org-timegrid-agenda-minutes-before / -after |
180 / 180 |
Visible window around now |
org-timegrid-agenda-insert-after |
"To Refile" |
Block above the strip |
org-timegrid-agenda-separator |
t |
Separator after the strip |
org-timegrid-compact-pixels-per-minute |
0.95 |
Strip scale |
org-timegrid-compact-font-size |
11 |
Strip text size |
| Org variable | Default | Meaning |
|---|---|---|
org-timegrid-org-files |
agenda |
Files to query; a list is literal |
org-timegrid-org-capture-file |
calendar.org in user-emacs-directory |
Target for new entries; always queried |
org-timegrid-org-capture-template |
top-level title and time range | Structure and location inside the capture file |
org-timegrid-org-auto-save |
nil |
Save the affected Org file after every calendar edit |
org-timegrid-org-show-done |
t |
Show timestamps belonging to headings in a done state |
org-timegrid-org-show-repeaters |
t |
Show repeater anchors and their generated occurrences |
org-timegrid-org-exclude-tags |
nil |
Hide headings carrying any listed tag |
org-timegrid-org-exclude-todo-states |
nil |
Hide headings whose TODO keyword is listed |
org-timegrid-org-exclude-properties |
nil |
Hide headings matching (PROPERTY . VALUE); nil matches any value |
org-timegrid-org-filter-function |
org-timegrid-org-default-filter |
Keep-predicate called with headline and timestamp Org elements |
org-timegrid-org-tag-color-alist |
nil |
Tag-to-colour mapping |
On an empty slot, RET and mouse-drag creation complete against unfinished
TODO headings in the configured Org files. Choosing one adds the new time
range to that heading. Entering text that does not match a candidate creates a
heading in org-timegrid-org-capture-file, as before. This uses Org's parser
and works with any completion-styles.
The default template is (:target file :template "* %{title}\n%{time-range}\n%?").
Its target may also be datetree, (headline "Name"), or
(olp "Parent" "Child"). Template headings are relative to that target. The
available substitutions are %{title}, %{time-range}, %{start}, %{end},
%{duration}, %<FORMAT> (using the selected start time), and %? for the
final point.
To keep calendar entries in a dedicated file organized by their start date:
(setq org-timegrid-org-capture-file "~/org/timegrid.org"
org-timegrid-org-capture-template
'(:target datetree
:template "* %{title}\n%{time-range}\n%?"))Creating “Project review” on August 25, 2026 produces a child of that date:
* 2026
** 2026-08 August
*** 2026-08-25 Tuesday
**** Project review
<2026-08-25 Tue 10:00-11:00>
The datetree date comes from the block's selected start time, including when
creating an entry in a past or future week. The capture file is also searched
and displayed automatically; it need not be added to org-agenda-files.
org-timegrid-org-after-create-hook runs with point on a newly created Org
heading. Hook edits join the same undo step.
(add-hook 'org-timegrid-org-after-create-hook
(lambda ()
(org-set-property
"CREATED_AT" (format-time-string "[%F %a %R]"))))The renderer itself does not require Org. org-timegrid-open accepts an
org-timegrid-backend with a listing function and optional callbacks for
create, update, delete, undo, visit, entry completion, and date input. A
backend with only a listing function is read-only. See
org-timegrid-backend-create for the full contract.
Create and update callbacks can accept a final time-kind argument whose
value is timed or all-day. This distinguishes date-only entries from timed
entries that happen to span whole days; callbacks using the older arity remain
supported.
The visual design and interaction borrow from macOS Calendar. org-timeblock
and timeblock supplied the idea of drawing an Emacs calendar with SVG, and
calfw and calfw-blocks were the original
inspiration toward a calendar view for Org.
Even though I directed the UI, interaction design, package structure, public commands, and customization options. Opus 5 and GPT-5.6-Codex wrote all code in the current implementation. The SVG renderer was agent-written and then reworked through hands-on usability testing.



