{"slug": "svg-line-better-status-bars-for-emacs", "title": "svg-line: Better Status Bars for Emacs", "summary": "Emacs developer Chiply released svg-line, a new package that renders all four Emacs status bars (mode-line, header-line, tab-bar, and tab-line) as SVG images to provide consistent multi-line layout, alignment, icons, and interactivity across them. The package overcomes native limitations that previously restricted features like multi-line support to only the tab-bar and icon display to only the mode-line and header-line. Users configure a single :content function and call svg-line-activate to enable the unified status bar system.", "body_md": "# svg-line: Better Status Bars for Emacs\n\n## Table of Contents\n\n## 1. TLDR\n\nEmacs provides four useful status bars (`mode-line`\n\n, `header-line`\n\n, `tab-bar`\n\n, and `tab-line`\n\n), but each imposes different, inconsistent limits on multi-line layout, alignment, icons, and interactivity. `svg-line`\n\n(see code on [GitHub](https://github.com/chiply/svg-line)) solves this by rendering them as SVG images, and normalizes a rich feature set across all status bars with a consistent configuration. `svg-line`\n\nworks by defining a small rendering engine built on Emacs's native SVG support. Configuring status bars is easy: you simply write one `:content`\n\nfunction and call `svg-line-activate`\n\n. You can see my custom configuration of [ mode-line](https://github.com/chiply/.zetta.d/blob/main/modules/ui/modeline-svg.el),\n\n[,](https://github.com/chiply/.zetta.d/blob/main/modules/ui/header-line-svg.el)\n\n`header-line`\n\n[, and](https://github.com/chiply/.zetta.d/blob/main/modules/ui/tab-line-svg.el)\n\n`tab-line`\n\n[in my](https://github.com/chiply/.zetta.d/blob/main/modules/ui/tab-bar-svg.el)\n\n`tab-bar`\n\n[Emacs config](https://github.com/chiply/.zetta.d).\n\nFigure 1: Every `*-line`\n\nin this frame is one SVG image drawn by `svg-line`\n\n.\n\n## 2. About\n\nEmacs gives us four status bars, the `mode-line`\n\n, the `header-line`\n\n, the `tab-bar`\n\n, and the `tab-line`\n\n(**-lines* for short). These are useful for providing a dynamic 'heads-up display', for important context, like what buffer you're in, the active major mode, and really any arbitrary thing you can define.\n\nI'm a heavy user of the *-lines in Emacs, and I have them all enabled, but the issue that has plagued me is that, natively, each one behaves differently and each has unique limitations. For example, multi-line status (necessary on my small laptop) is possible, but only in the `tab-bar`\n\n. Right alignment is possible in the `tab-bar`\n\n, but only in the last line, and this alignment feature is only available in the `tab-bar`\n\n. I can display icons from all-the-icons in the `mode-line`\n\nand `header-line`\n\n, but not the `tab-bar`\n\nor `tab-line`\n\n. Etc….\n\nWhat I really want is consistent behaviour and configuration across all these status bars, and I want the multi-line, alignment, and icons features available in all of them. It turns out that SVG ([scaled vector graphics](https://en.wikipedia.org/wiki/SVG)) is the key to solving this.\n\nInspired by Nicolas Rougier's [dual-header gist](https://gist.github.com/rougier/8d5a712aa43e3cc69e7b0e325c84eab4), I built `svg-line`\n\n, which provides this experience by utilizing Emacs's built-in SVG rendering support. At first, this approach seemed like a hack, or abuse of the *-lines, or neglect of the built-in status bar behaviour. But I kept it and created a package because I was literally shocked how well this works and how native this feels (see the screenshot and video above).\n\nNote that even if you only use the `mode-line`\n\n, `svg-line`\n\nis still useful — likely *more* so, since a single status bar has to render all your indicators on its own.\n\n## 3. `svg-line`\n\n's Features\n\n**Multi-line everywhere**, with per-row left/center/right alignment.** A**overflowing tabs onto new rows instead of hiding them, including with file-type glyphs, a current-tab highlight, and an unsaved tint.`tab-line`\n\nthat wraps**Clickable anything.** Any segment can carry a left-click action, a right-click menu, and hover help with a highlight. This works uniformly across all four bars, including the otherwise-uncooperative`tab-bar`\n\n.**Icons as text.** Using[Nerd Fonts](https://www.nerdfonts.com/#home)and an icon is just a character that flows with everything else. SVG rendering also enables a full-height \"masthead\" glyph option on status bars that can span multiple lines.**Dynamic and animated indicators**: a buffer-position pie, progress bars, active vs. inactive styling per window.** It respects text scale.**The bars track`text-scale`\n\n, re-rendering crisply instead of blurring.\n\nA meta feature is that the configuration surface is uniform across all status bars, which is a pleasant improvement over the diverse configuration strategies for the native APIs.\n\n## 4. Why SVG Works\n\nWhen using `svg-line`\n\n, each line becomes **one SVG image**, and SVG images are more featureful than the native text engine:\n\n**It can be any height.** Multi-row bars are now possible in every *-line.**Everything is placed at exact pixel coordinates.** Left, right, and center alignment work identically on every row.**It draws whatever you want.** Text, yes, but also wrapped tab flows, geometric progress bars and pies, and (with a Nerd Font) icon glyphs inline with the text, the same on all four lines. Anything you can render in an SVG (just about anything) is fair game.**The engine remembers where it drew.** It can detect the mouse against those placements, so clicks, right-click menus, and hover all work on any element of any line.\n\n## 5. Configuration\n\nConfiguring `svg-line`\n\nis deliberately simple. You write a `:content`\n\nfunction that returns rows, supply it to `svg-line-define`\n\n, and call `svg-line-activate`\n\non the defined line. This configuration pattern is identical for all four bars. The engine has two layouts: `lines`\n\n(the default — rows of segments, used for the `mode-line`\n\n, `header-line`\n\n, and `tab-bar`\n\n) and `wrap`\n\n(a flow that wraps, used for the `tab-line`\n\n).\n\n### 5.1. Mode-line\n\n#### 5.1.1. Simple `mode-line`\n\nThe smallest useful line is a single row: a label on the left, the cursor position on the right.\n\n```\n(svg-line-define 'my-mode-line\n  :target 'mode-line\n  :content (lambda ()\n             ;; one row: (LEFT-SEGMENTS . RIGHT-SEGMENTS)\n             (list (cons (list (buffer-name))\n                         (list (format-mode-line \"%l:%c\"))))))\n\n(svg-line-activate 'my-mode-line)\n```\n\nThis trivial example clarifies the pattern: `define`\n\nthen `activate`\n\n:\n\n`:content`\n\nis the only required key: a function returning a list of*rows*. Each row is a`(LEFT . RIGHT)`\n\ncons, and each side is a*list of segments*— here just plain strings.- with no\n`:background`\n\n,`:foreground`\n\n, or`:active`\n\n, the line picks sensible defaults and is always drawn as active. `svg-line-activate`\n\nenables it, and`svg-line-deactivate`\n\n/`svg-line-toggle`\n\ndisable it, restoring the native`mode-line`\n\nuntouched.\n\n#### 5.1.2. Rich `mode-line`\n\nHere's a more complicated `mode-line`\n\nconfiguration that demonstrates `svg-line`\n\n's feature scope. It defines two rows, three-way alignment, a masthead icon, a custom segment, a clickable button, dynamic theme colours, and active/inactive styling:\n\n```\n;; A custom segment is just a zero-argument function returning a string.\n;; This one shows how far point sits through the buffer, as a percentage.\n(defun my/buffer-percent ()\n  (format \" %d%%\" (/ (* 100 (point)) (max 1 (point-max)))))\n\n(svg-line-define 'my-mode-line\n  :target     'mode-line\n  :active     #'mode-line-window-selected-p\n  :icon       (lambda () (nerd-icons-icon-for-mode major-mode))\n  :background (lambda () (face-background 'mode-line nil t))\n  :foreground (lambda () (face-foreground 'default nil t))\n  :content\n  (lambda ()\n    (list\n     ;; row 1 — three independently-aligned segments on one row\n     (list :left   (list (buffer-name))\n           :center (list (symbol-name major-mode))\n           :right  (list (format-time-string \"%H:%M\")))\n     ;; row 2 — custom segment + position on the left, a button on the right\n     (cons (list #'my/buffer-percent (format-mode-line \" %l:%c\"))\n           (list (svg-line-seg \"save\"\n                               :id 'ml-save\n                               :help \"buffer actions\"\n                               :action #'save-buffer\n                               :action-help \"save\"\n                               :menu '((\"Revert\" . revert-buffer)\n                                       (\"Kill\"   . kill-current-buffer))))))))\n\n(svg-line-activate 'my-mode-line)\n```\n\nLine by line:\n\n`my/buffer-percent`\n\n— any zero-argument function can be a segment; this one returns a string.`:active #'mode-line-window-selected-p`\n\n— a predicate; when it's false (an unfocused window) the engine applies the`:inactive-*`\n\ncolours instead.`:icon`\n\n— a full-height \"masthead\" glyph drawn once on the left edge, spanning*both*rows. This is a function, so it tracks the current buffer's mode.`:background`\n\n/`:foreground`\n\n— literal colours, or (as here) zero-argument functions evaluated on every render, so the bar follows your theme automatically.*row 1*— a`:left/:center/:right`\n\nplist puts three independently-aligned segments on a single row.*row 2*— a plain`(LEFT . RIGHT)`\n\ncons. Its left side mixes the custom*function*with an ordinary`%l:%c`\n\nstring.`svg-line-seg`\n\n— turns a segment into a button: left-click runs`:action`\n\n, right-click opens the`:menu`\n\n, and`:help`\n\nshows on hover in the echo area.\n\n### 5.2. Tab-line\n\nThe `tab-line`\n\nis where the `wrap`\n\nlayout is most useful: instead of scrolling overflow off the edge, it flows tabs onto subsequent rows.\n\n```\n(svg-line-define 'my-tab-line\n  :target  'tab-line\n  :layout  'wrap\n  :content (lambda ()\n             ;; each item is (LABEL . STATE)\n             (mapcar (lambda (buf)\n                       (cons (buffer-name buf)\n                             (list :current  (eq buf (current-buffer))\n                                   :modified (buffer-modified-p buf))))\n                     (tab-line-tabs-window-buffers)))\n  :current-background  (lambda () (face-background 'highlight nil t))\n  :modified-foreground \"#ebcb8b\")\n\n(svg-line-activate 'my-tab-line)\n```\n\n`:layout 'wrap`\n\n— switches from rows of segments to a wrapping flow; overflowing tabs land on a new row rather than scrolling out of sight.- each item is\n`(LABEL . STATE)`\n\n, where`:current`\n\nand`:modified`\n\nin the state plist drive the per-tab highlight and unsaved tint. `:current-background`\n\n/`:modified-foreground`\n\n— the same value-or-function styling as the`lines`\n\nlayout, just with current- and modified-tab variants.\n\n## 6. Acknowledgement\n\nCredit where it's due: this started as an experiment off [Nicolas Rougier](https://github.com/rougier)'s work. His SVG explorations and that dual-header gist demonstrated that this was possible, and showed me how well this approach works.", "url": "https://wpnews.pro/news/svg-line-better-status-bars-for-emacs", "canonical_source": "https://www.chiply.dev/post-svg-line", "published_at": "2026-06-08 11:58:29+00:00", "updated_at": "2026-06-11 20:18:26.243886+00:00", "lang": "en", "topics": ["ai-tools"], "entities": ["Emacs", "svg-line", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/svg-line-better-status-bars-for-emacs", "markdown": "https://wpnews.pro/news/svg-line-better-status-bars-for-emacs.md", "text": "https://wpnews.pro/news/svg-line-better-status-bars-for-emacs.txt", "jsonld": "https://wpnews.pro/news/svg-line-better-status-bars-for-emacs.jsonld"}}