My Emacs Blog
29 Mar 2025

The TAB Key in Org Mode, Reimagined

I don't know about you, but when I'm reading something in an Org file and spot a link I want to follow, I instinctively press TAB to jump to it—just like I would in an Info or Help buffer. Using TAB for such field navigation is a common pattern across many applications, including Emacs. It’s also nicely symmetric with Shift-TAB (S-TAB), which typically navigates backward. But in Org mode, TAB triggers local visibility cycling: folding and unfolding text under the current headline. S-TAB cycles visibility globally, folding and unfolding all the headlines. (Granted, if you don’t use Info or navigate Help buffers with TAB, you might not miss that behavior in Org mode.)

See, we have this dichotomy in Org mode: it's both an authoring tool and a task/notes manager. For document authoring, Org markup serves as a source format that's later exported for publishing. In this context, visibility cycling is essential for managing structure and reducing distractions while writing. As a task and notes manager, Org is used to track notes, TODO lists, schedules, and data—content that's often read in place and never exported. Visibility cycling still helps, but it's generally less critical than in authoring mode.

This reading workflow within Org files makes me long for features found in more reading-focused modes. Sure, I don’t treat my Org files as read-only; reading and editing are fluidly intertwined. Still, when I'm focused on reading, I want the TAB key to handle navigation, not headline visibility cycling. And I don't want to switch to another mode like View mode just to get a better reading experience.

It's well known that the TAB key is heavily overloaded in Emacs, especially in Org mode. Depending on context and configuration, it can perform one of four types of actions: line indentation, candidate completion (during editing), or field navigation and visibility cycling (during reading). As mentioned earlier, TAB is commonly used for field navigation in Info and Help modes. But note that even Org mode uses it this way within tables. Its association with visibility cycling was unique to Org mode until recently, when it was made an option in Outline mode too.

Personally, I want to move in the opposite direction: removing visibility cycling from the list of TAB-triggered actions. Three types of behavior are already plenty. I'd rather assign visibility control to a more complex keybinding and prioritize field navigation instead. I'm not a big fan of cycling in general (see my previous blog post), and would prefer to jump directly to specific folding levels. I also value consistency in keybindings, so unifying TAB behavior across modes is important to me.

TAB: indentation and completion when editing; field navigation when reading

I decided to give it a try and remap TAB in Org mode to primarily perform field navigation. What exactly is considered a “field” is largely up to the user. In general, it should be a structural element in a file where a non-trivial action can be performed, making it useful to have an easy way to jump between them. For my setup, I chose to treat only links and headlines as fields, similar to how Info handles navigation. Of course, others might include property drawers, code blocks, custom buttons, or other interactive elements. I wouldn't overdo it though—too many fields and TAB navigation loses its utility.

I remapped TAB in Org mode to navigate to the next visible heading or link, and S-TAB to move to the previous one. Headlines and links inside folded sections are skipped. For visibility cycling, I now rely on Org Speed Keys (a built in feature of Org mode).

Speed Keys let you trigger commands with a single keystroke when the point is at the beginning of a headline. They’re off by default but incredibly handy once enabled. A number of keys are predefined out of the box; for example, c is already mapped to org-cycle, which is what TAB normally does in Org mode.

I’ve had Speed Keys enabled for ages (mainly using them for forward/backward headline navigation), but I had never used c for visibility cycling—until now. And it gets even better: the combination of TAB / S-TAB to jump between fields, followed by a speed key at the headline, turns out to be quite powerful.

What about the other actions TAB usually performs in Org files? For now, I rely on M-x org-cycle when needed. The org-cycle command is quite sophisticated and can fall back to other TAB behaviors like indentation when appropriate. That said, I’ve been using my custom TAB / S-TAB bindings for months now and haven’t run into any situations where I missed the default behavior.

Want to give it a try? Here’s the code you can drop into your init.el:

(defun /org-next-visible-heading-or-link (&optional arg)
  "Move to the next visible heading or link, whichever comes first.
With prefix ARG and the point on a heading(link): jump over subsequent
headings(links) to the next link(heading), respectively.  This is useful
to skip over a long series of consecutive headings(links)."
  (interactive "P")
  (let ((next-heading (save-excursion
                        (org-next-visible-heading 1)
                        (when (org-at-heading-p) (point))))
        (next-link (save-excursion
                     (when (/org-next-visible-link) (point)))))
    (when arg
      (if (and (org-at-heading-p) next-link)
          (setq next-heading nil)
        (if (and (looking-at org-link-any-re) next-heading)
            (setq next-link nil))))
    (cond
     ((and next-heading next-link) (goto-char (min next-heading next-link)))
     (next-heading (goto-char next-heading))
     (next-link (goto-char next-link)))))

(defun /org-previous-visible-heading-or-link (&optional arg)
  "Move to the previous visible heading or link, whichever comes first.
With prefix ARG and the point on a heading(link): jump over subsequent
headings(links) to the previous link(heading), respectively.  This is useful
to skip over a long series of consecutive headings(links)."
  (interactive "P")
  (let ((prev-heading (save-excursion
                        (org-previous-visible-heading 1)
                        (when (org-at-heading-p) (point))))
        (prev-link (save-excursion
                     (when (/org-next-visible-link t) (point)))))
    (when arg
      (if (and (org-at-heading-p) prev-link)
          (setq prev-heading nil)
        (if (and (looking-at org-link-any-re) prev-heading)
            (setq prev-link nil))))
    (cond
     ((and prev-heading prev-link) (goto-char (max prev-heading prev-link)))
     (prev-heading (goto-char prev-heading))
     (prev-link (goto-char prev-link)))))

;; Adapted from org-next-link to only consider visible links
(defun /org-next-visible-link (&optional search-backward)
  "Move forward to the next visible link.
When SEARCH-BACKWARD is non-nil, move backward."
  (interactive)
  (let ((pos (point))
        (search-fun (if search-backward #'re-search-backward
                      #'re-search-forward)))
    ;; Tweak initial position: make sure we do not match current link.
    (cond
     ((and (not search-backward) (looking-at org-link-any-re))
      (goto-char (match-end 0)))
     (search-backward
      (pcase (org-in-regexp org-link-any-re nil t)
        (`(,beg . ,_) (goto-char beg)))))
    (catch :found
      (while (funcall search-fun org-link-any-re nil t)
        (let ((folded (org-invisible-p nil t)))
          (when (or (not folded) (eq folded 'org-link))
            (let ((context (save-excursion
                             (unless search-backward (forward-char -1))
                             (org-element-context))))
              (pcase (org-element-lineage context '(link) t)
                (link
                 (goto-char (org-element-property :begin link))
                 (throw :found t)))))))
      (goto-char pos)
      ;; No further link found
      nil)))

(defun /org-shifttab (&optional arg)
  "Move to the previous visible heading or link.
If already at a heading, move first to its beginning.  When inside a table,
move to the previous field."
  (interactive "P")
  (cond
   ((org-at-table-p) (call-interactively #'org-table-previous-field))
   ((and (not (bolp)) (org-at-heading-p)) (beginning-of-line))
   (t (call-interactively #'/org-previous-visible-heading-or-link))))

(defun /org-tab (&optional arg)
  "Move to the next visible heading or link.
When inside a table, re-align the table and move to the next field."
  (interactive)
  (cond
   ((org-at-table-p) (org-table-justify-field-maybe)
    (call-interactively #'org-table-next-field))
   (t (call-interactively #'/org-next-visible-heading-or-link))))

(use-package org
  :config
  ;; RET should follow link when possible (moves to next field in tables)
  (setq org-return-follows-link t)
  ;; must be at the beginning of a headline to use it; ? for help
  (setq org-use-speed-commands t)
  ;; Customize some bindings
  (define-key org-mode-map (kbd "<tab>") #'/org-tab)
  (define-key org-mode-map (kbd "<backtab>") #'/org-shifttab)
  ;; Customize speed keys: modifying operations must be upper case
  (custom-set-variables
   '(org-speed-commands
     '(("Outline Navigation and Visibility")
       ("n" . (org-speed-move-safe 'org-next-visible-heading))
       ("p" . (org-speed-move-safe 'org-previous-visible-heading))
       ("f" . (org-speed-move-safe 'org-forward-heading-same-level))
       ("b" . (org-speed-move-safe 'org-backward-heading-same-level))
       ("u" . (org-speed-move-safe 'outline-up-heading))
       ("j" . org-goto)
       ("c" . org-cycle)
       ("C" . org-shifttab)
       (" " . org-display-outline-path)
       ("s" . org-toggle-narrow-to-subtree)
       ("Editing")
       ("I" . (progn (forward-char 1) (call-interactively 'org-insert-heading-respect-content)))
       ("^" . org-sort)
       ("W" . org-refile)
       ("@" . org-mark-subtree)
       ("T" . org-todo)
       (":" . org-set-tags-command)
       ("Misc")
       ("?" . org-speed-command-help))))
  )

A few comments about the code, for those interested:

  1. This is more of a proof-of-concept than optimized code ready for upstreaming.
  2. My /org-next-visible-link is a simplified version of the built-in org-next-link, tailored to the specific cases I care about. Honestly, I was surprised that org-next-link doesn’t already do what I need. It jumps to the next link even if it’s inside a folded section, causing it to unfold. I have a hard time imagining why would anyone need that.
  3. In /org-tab and /org-shifttab, I preserved the default behavior of org-cycle within a table: it navigates between table fields.
  4. I’ve also customized org-speed-commands to only bind editing actions to keys that require the Shift modifier. I like keeping lowercase keys reserved for non-destructive commands. As a next step, I may remap Space and Shift-Space to scroll the buffer. That would bring me even closer to a more consistent reading experience.

Enjoy the malleability of Emacs and the freedom it gives you!

Discuss this post on Reddit.

Tags: emacs
Other posts
© 2025 Peter Povinec. This work is licensed under CC BY-SA 4.0