Back in 2018 I posted about making tabbar.el look pretty. Emacs 27 includes two built-in ways to display tabs:

  1. tab-bar-mode works per frame to show window configurations
  2. tab-line-mode works per window to show buffers

Tab-line mode seems similar to tabbar.el, so I decided to switch from tabbar.el to tab-line.

Screenshot of emacs 27 tab-line-mode
Emacs 27 tab-line mode

I had customized tabbar.el to show only the buffers from the current project, sorted alphabetically. It's even easier with tab-line: tab-line-tabs-function can be set to group mode, which uses tab-line-tabs-buffer-group-function to choose the group name and tab-line-tabs-buffer-group-sort-function to sort the buffers in the group.

(require 's)
(defun my/tab-line-buffer-group (buffer)
  "Use the project.el name for the buffer group"
  (with-current-buffer buffer
    (s-chop-suffix "/" (car (project-roots (project-current))))))

(defun my/buffer-sort (a b) (string< (buffer-name a) (buffer-name b)))
(setq tab-line-tabs-buffer-group-sort-function #'my/buffer-sort)
(setq tab-line-tabs-buffer-group-function #'my/tab-line-buffer-group)
(setq tab-line-tabs-function #'tab-line-tabs-buffer-groups)

Next, I wanted to make the tabs look nicer. As before, I used powerline for this. I disabled the extra buttons by setting tab-line-new-button-show and tab-line-close-button-show to nil, then set tab-line-tab-name-function to a function that adds powerline wave symbols to both sides of the tab name:

(require 'powerline)
(defvar my/tab-height 22)
(defvar my/tab-left (powerline-wave-right 'tab-line nil my/tab-height))
(defvar my/tab-right (powerline-wave-left nil 'tab-line my/tab-height))

(defun my/tab-line-tab-name-buffer (buffer &optional _buffers)
  (powerline-render (list my/tab-left
                          (format " %s  " (buffer-name buffer))
(setq tab-line-tab-name-function #'my/tab-line-tab-name-buffer)
(setq tab-line-new-button-show nil)
(setq tab-line-close-button-show nil)

I used a color scheme that's consistent with my modeline:

(set-face-attribute 'tab-line nil ;; background behind tabs
      :background "gray40"
      :foreground "gray60" :distant-foreground "gray50"
      :family "Fira Sans Condensed" :height 1.0 :box nil)
(set-face-attribute 'tab-line-tab nil ;; active tab in another window
      :inherit 'tab-line
      :foreground "gray70" :background "gray90" :box nil)
(set-face-attribute 'tab-line-tab-current nil ;; active tab in current window
      :background "#b34cb3" :foreground "white" :box nil)
(set-face-attribute 'tab-line-tab-inactive nil ;; inactive tab
      :background "gray80" :foreground "black" :box nil)
(set-face-attribute 'tab-line-highlight nil ;; mouseover
      :background "white" :foreground 'unspecified)

Finally, I want to use keys to navigate the tabs. I bound H-j to tab-line-switch-to-prev-tab and H-k to tab-line-switch-to-next-tab. I use these same keys for terminals and web browsers.

I'm pretty happy with tab-line mode so far.



Amit wrote at Sunday, July 5, 2020 at 12:37:00 PM PDT

One thing that confuses me is that tab-line-tab-current is supposed to be for the active window but I often find the active window has the tab-line-tab color. For now I'm going to set these two to the same.

Anonymous wrote at Saturday, December 5, 2020 at 12:00:00 PM PST

Hi Amit,

executing your code gives me a
Symbol's function definition is void: hsl

I saw a package named hsluv, installed it, changed your code snippet from "hsl" to "hsluv", but received a similar message:
Symbol's function definition is void: hsluv

What is hsl?

Best regards,

Amit wrote at Thursday, December 17, 2020 at 9:20:00 PM PST

Oh! Sorry about that!

(require 'color)
(defun hsl (H S L)
"Convert HSL to a #rrggbb string, using HSL 0-360, S 0-1, L 0-1"
(apply 'color-rgb-to-hex (color-hsl-to-rgb (/ H 360.0) S L)))

I will update the blog post.

Anonymous wrote at Friday, December 18, 2020 at 5:58:00 AM PST

Why switch from tabbar.el? Is it because tab-line is built-in, or is there some reason that tab-line fits your workflow better?

Xavier Limón wrote at Sunday, December 20, 2020 at 7:50:00 PM PST

Great post and very useful.

Quick question: I have seen that the waves powerline leaves a gray space where the tab rectangle is cut, how can I change the color of that gray space? I fiddled with the tab-line faces but I cannot find where the color is defined.

Thanks in advance.

Amit wrote at Tuesday, December 22, 2020 at 7:41:00 PM PST

Anonymous: tabbar.el uses the header line, and there are other packages that also want to use the header line, so I thought it would be nice to switch to tab-line so that I could use those other packages. For example lsp-mode uses the header line to show "breadcrumbs", and I think magit also has some code to use the header line.

Xavier: it seems like some platforms have color differences that might be what you're seeing. If you're on mac, try either (setq ns-use-srgb-colorspace t) or (setq ns-use-srgb-colorspace nil) to see if that makes a difference. I've also heard it affects Linux but haven't had the problem on my Linux system.

Anonymous wrote at Thursday, February 4, 2021 at 10:11:00 AM PST

Your point about the header-line is well taken, but I don't like how tab-line mode works.

I took tabbar.el and changed all instances of header-line to tab-line. It worked perfectly. It still uses the tabbar faces.

Alexandre Rousseau wrote at Friday, May 14, 2021 at 4:09:00 AM PDT

Beautiful. How did you manage to customize the line numbers gutter? I can't find the face property for that.

Amit wrote at Thursday, May 27, 2021 at 2:40:00 PM PDT

Thanks Alexandre! I set the line-number, line-number-current-line, and line-number-major-tick faces to be a small font, and I set display-line-numbers-width to 4, and display-line-numbers-major-tick to 5.