Setting up Emacs native tab-bar and tab-bar-groups for a tmux-like experience

Cover Image for Setting up Emacs native tab-bar and tab-bar-groups for a tmux-like experience
Rahul M. Juliato
Rahul M. Juliato
#emacs#tab-bar# groups# tmux

Explore how to turn Emacs' native tab-bar and tab-bar-groups into a powerful, tmux-like window and session management experience—no external packages needed. Organize your workflows with tabs, group them by project or context, and navigate with ease inside your Emacs session, all while keeping tmux nearby for when it still shines.

Here I'm traversing an open session using this concept of organization by simply issuing C-TAB.

tab-bar-config-demo

If you prefer not to show the group name, want to display buffer names, use other custom decorations, jump right into your group, don’t worry, we’ll explore all these possibilities step by step.

Now, how do we achieve this! 🤩


Motivation

It’s no secret that many Emacs users take advantage of its excellent window management capabilities, like: splitting windows, saving layouts, undoing and redoing them and even use tab-bar as a sort of tmux-like workflow.

What I’m presenting here takes it a step further: bringing in a "split by session" feature, just like tmux UI. In other words, we’re expanding our window management arsenal with:

➖ Tabs, as in Emacs we call it tab-bar (not to be confused with the VSCode-like tab-line mode): which can hold splits of different buffers, either in the same file, different files, terminals, and everything else Emacs can print in a buffer.

➖ Tab Groups, which can hold groups of tabs, mimicking sessions as we call them in tmux, or perspectives if you know this concept from persp-mode or perspective-el, or even activities if you use Doom Emacs.

Also, did I mention we're going to do it without any external package dependencies?

With the provided configuration, we're going to organize our current running Emacs session in "two levels":

The 'tab-bar-groups'

This level holds the tab-group. This might contain a "topic" like "Chat", "Mail" or "News", or simply your project name like "My Project", or if you're working with multiple projects at the same time, one level that might be organized by "Your Workflow". And of course, you can have all of this at the same time, like:

tab-bar-groups

The 'tab-bars'

This level contains your tabs, which can hold all sorts of window arrangements (for the uninitiated, from Emacs's point of view, the OS- level 'window' that holds the Emacs application is called a 'frame', while 'windows' are the inner splits that hold our buffers).

tab-bar-groups-group


So, first things first. I'm reproducing here the steps to the final form I just showed. But of course, it is all customizable. Want to do another sort of decorations? Want to hide the group name? Want to show filenames? Want to navigate differently? Go for it! It is all transparent to you!

Variables configurations

This is personal taste, take a look at each variable's documentation and tweak it yourself, basically:

➖ I do not want the close button, nor the new button, as I seldom use mouse navigation.

➖ I do want tab-hints, which are numbers on each tab-name for better navigation. I do override the internal function, though, to get it "decorated" my way.

➖ I want a clean separator, so, a single space.

➖ We want the tab-group name shown, hence we add to tab-bar-format the tab-bar-format-tabs-groups option.

All of this can be defined with:

(setq tab-bar-close-button-show nil)
  (setq tab-bar-new-button-show nil)
  (setq tab-bar-tab-hints t)
  (setq tab-bar-auto-width nil)
  (setq tab-bar-separator " ")
  (setq tab-bar-format '(tab-bar-format-tabs-groups
					tab-bar-format-tabs tab-bar-separator
					tab-bar-format-add-tab))

A few (IMHO justified) overrides

Tab bar doesn't allow us many customizations. Fortunately, we can override a couple of functions as they're small and easy to keep up with. Of course, this is totally optional; I'm just trying to mimic a more tmux-like UI feel.

First, tab-bar-tab-name-format-hints: I want to put some arrows around the hints number, and NOT show the buffer name.

(defun tab-bar-tab-name-format-hints (name _tab i)
	  (if tab-bar-tab-hints (concat (format "»%d«" i) "") name))

Second, tab-bar-tab-group-format-default: By default, groups show the hint of the first tab under it. I want a clean group name, so:

(defun tab-bar-tab-group-format-default (tab _i &optional current-p)
	(propertize
	 (concat (funcall tab-bar-tab-group-function tab))
	 'face (if current-p 'tab-bar-tab-group-current 'tab-bar-tab-group-inactive)))

Nice QoL Utility functions

With the above config, we can already do something like C-x t G, setting a group name for your current tab and start organizing your life!

You could also have automatically groups created by setting tab-group in your display-buffer-alist, like:

(add-to-list 'display-buffer-alist
			   '("\\*scratch\\*"
				 (display-buffer-in-tab display-buffer-full-frame)
				 (tab-group . "[EMACS]")))

We're not focusing on automatically tab-grouping stuff in this post though.

Truth is, yes, I want groups for my News, Mail, Chat, but most of my work is done in the form of Projects.

And yes, I want these settings to be manually issued. I can recall the pain of having to sneak-peak another project utility function or doc, just to have my crazy custom persp-mode pulling a new persp and messing with everything.

Function to set tab to group based on project

So, I want a function that can "promote" my current tab to the group [ProjectName], creating it if there are none. Of course, if the current buffer is part of a project. This allows me to switch projects, open new splits, without automagic jumps.

Here we have a function to do so, and a suggested bind:

(defun emacs-solo/tab-group-from-project ()
	"Call `tab-group` with the current project name as the group."
	(interactive)
	(when-let* ((proj (project-current))
				(name (file-name-nondirectory
					   (directory-file-name (project-root proj)))))
	  (tab-group (format "[%s]" name))))

  (global-set-key (kbd "C-x t P") #'emacs-solo/tab-group-from-project)

So, recap: I can C-x t G and "add" my tab to a group, and now I can also simply C-x t P and "add" my tab to the project group.

😎 Workflow?

C-x t p p: starts a new tab selecting a project

➖ Select a file, dired or shell...

C-x t P: add your new tab to the project group, creating it

Want some more tabs?

C-x t 2 will automatically add tabs to your current group.

Isn't it nice? Now, you can feel the power in your hands, you open 10 projects, you create a bunch of groups for your inner Emacs is my OS workflow, how do you traverse all this madness?

Function to jump to group

I found my self abusing of the default C-TAB and C-S-TAB to quickly "jump" between closer tabs. Now, I wanna quickly check my Mail, I'd like something more "precise" jumping than eye balling everything.

This is were our second utility function comes to hand:

(defun emacs-solo/tab-switch-to-group ()
  "Prompt for a tab group and switch to its first tab.
Uses position instead of index field."
  (interactive)
  (let* ((tabs (funcall tab-bar-tabs-function)))
	(let* ((groups (delete-dups (mapcar (lambda (tab)
										  (funcall tab-bar-tab-group-function tab))
										tabs)))
		   (group (completing-read "Switch to group: " groups nil t)))
	  (let ((i 1) (found nil))
		(dolist (tab tabs)
		  (let ((tab-group (funcall tab-bar-tab-group-function tab)))
			(when (and (not found)
					   (string= tab-group group))
			  (setq found t)
			  (tab-bar-select-tab i)))
		  (setq i (1+ i)))))))
  (global-set-key (kbd "C-x t g") #'emacs-solo/tab-switch-to-group)

This allows us to "list all available groups", select and switch to the first tab of that group.

tab-bar-group-change

Packing the entire config

The code here presented by parts is now part of my emacs-solo config (hence the prefix on the function names), I usually keep my configuration somewhat organized by use-package blocks, they keep everything in the right place and I suggest you do the same. Also it is a lot faster to grab this code, copy and paste to your config and make it work!

(use-package tab-bar
  :ensure nil
  :defer t
  :custom
  (tab-bar-close-button-show nil)
  (tab-bar-new-button-show nil)
  (tab-bar-tab-hints t)
  (tab-bar-auto-width nil)
  (tab-bar-separator " ")
  (tab-bar-format '(tab-bar-format-tabs-groups
					Tab-bar-format-tabs tab-bar-separator
					tab-bar-format-add-tab))
  :init
  ;;; --- OPTIONAL INTERNAL FN OVERRIDES TO DECORATE NAMES
  (defun tab-bar-tab-name-format-hints (name _tab i)
	  (if tab-bar-tab-hints (concat (format "»%d«" i) "") name))

  (defun tab-bar-tab-group-format-default (tab _i &optional current-p)
	(propertize
	 (concat (funcall tab-bar-tab-group-function tab))
	 'face (if current-p 'tab-bar-tab-group-current 'tab-bar-tab-group-inactive)))


  ;;; --- UTILITIES FUNCTIONS
  (defun emacs-solo/tab-group-from-project ()
	"Call `tab-group` with the current project name as the group."
	(interactive)
	(when-let* ((proj (project-current))
				(name (file-name-nondirectory
					   (directory-file-name (project-root proj)))))
	  (tab-group (format "[%s]" name))))

  (defun emacs-solo/tab-switch-to-group ()
  "Prompt for a tab group and switch to its first tab.
Uses position instead of index field."
  (interactive)
  (let* ((tabs (funcall tab-bar-tabs-function)))
	(let* ((groups (delete-dups (mapcar (lambda (tab)
										  (funcall tab-bar-tab-group-function tab))
										tabs)))
		   (group (completing-read "Switch to group: " groups nil t)))
	  (let ((i 1) (found nil))
		(dolist (tab tabs)
		  (let ((tab-group (funcall tab-bar-tab-group-function tab)))
			(when (and (not found)
					   (string= tab-group group))
			  (setq found t)
			  (tab-bar-select-tab i)))
		  (setq i (1+ i)))))))

  ;;; --- EXTRA KEYBINDINGS
  (global-set-key (kbd "C-x t P") #'emacs-solo/tab-group-from-project)
  (global-set-key (kbd "C-x t g") #'emacs-solo/tab-switch-to-group)

  ;;; --- TURNS ON BY DEFAULT
  (tab-bar-mode 1))

Customizations on tab-bar-properties

You might want to customize the tab-bar line, what I am using in these screenshots is:

(custom-set-faces
  '(tab-bar
	((t (:background "#232635" :foreground "#A6Accd"))))
  '(tab-bar-tab
	((t (:background "#232635" :underline t))))
  '(tab-bar-tab-inactive
	((t ( ;; :background "#232635" ;; uncomment to use this
		  ;; :box (:line-width 1 :color "#676E95")
		  ))))
  '(tab-bar-tab-group-current
	((t (:background "#232635" :foreground "#A6Accd" :underline t))))
  '(tab-bar-tab-group-inactive
	((t (:background "#232635" :foreground "#777")))))

So, time to ditch tmux?

I wish...

This functionality is indeed very useful, the UI mimics tmux-like power. And if this is enough for you, go for it! Ditch tmux!

For my use cases, the sheer possibility of any of my emacs-lisp code locking the one and only Emacs process means my beautifully designed and crafted Emacs session is going bye-bye with it. And yes, while emacs --daemon and restarting clients helps a lot here, let’s not pretend Emacs never goes sideways.

There are still solid reasons to keep tmux around:

Fault tolerance. When you’re SSH’d into a remote machine and something crashes, tmux is still there, your shell lives on. Emacs tabs don’t protect you from network drops or X11/Wayland hiccups.

Shell multiplexing. Sometimes you just want 3 quick shells, nothing fancy, don’t even want to boot up Emacs. tmux wins here. Fast, lightweight, and scriptable. You just install tmux, no fancy config needed to 'just use it'.

System-level process separation. I like to keep long-running REPLs, tailing logs, or even a docker attach session in tmux. If Emacs dies, they don’t.

Startup time. Emacs with heavy configuration can still take a second to feel "fully alive". When I want to attach to a ready-to-go shell session instantly, tmux a is just faster.

Better separation. While the whole tab-bar and tab-group approach is super flexible, sometimes you just need the hard boundary of a terminal session completely isolated from the rest. There are things you do outside Emacs for good reason.

And let’s be honest, you don’t need to choose. These tools complement each other. What this configuration gives you is a powerful Emacs-as-an-OS experience, with clarity, agility, and a clean mental model. Use Emacs for your inner workflows, and tmux as your outer shell guardian.


Wrapping Up

With just a few lines of Elisp, no external packages, and some clever overriding, Emacs’ tab-bar and tab-bar-groups become serious productivity tools. If you’re someone juggling multiple projects, workflows, or simply enjoy clean organization inside your Emacs session, this setup gives you control and clarity.

While we might not throw tmux out of the toolbox just yet, we now have a native Emacs experience that feels modern, fast, and surprisingly intuitive. Use what’s best for your workflow, but know that Emacs is more than capable of stepping up its game.

So go ahead, give it a try, tweak it, theme it, and make Emacs your tmux... and more.

Happy hacking. ✨💻🤓🚀