Crafting Your Own Snippets with Emacs Built-In Abbrev Mode

Cover Image for Crafting Your Own Snippets with Emacs Built-In Abbrev Mode
Rahul M. Juliato
Rahul M. Juliato
#emacs#template# completion

In the world of text editing and programming, templating is a superpower. It saves keystrokes, reduces errors, and lets you focus on the bigger picture. For Emacs users, battle-tested packages like yasnippet and tempel are often the go-to solutions, offering powerful features and extensive libraries.

But what if you're in the mood for a little crafting? What if you want a solution that's built right into Emacs, requires no third-party installations, and can be easily copied and pasted into any configuration?

The all mighty built-in abbrev-mode is there for all of us Emacs users! In this post, we'll explore how you can leverage this humble, built-in minor mode to create a powerful, progressive, and deeply personal (for good or bad) snippet system.

What is Abbrev Mode?

At its core, abbrev-mode is a simple expansion system. You define an "abbreviation" (a short string) and an "expansion" (the text it should be replaced with). When the mode is active, it can automatically replace the abbrev with its expansion as you type.

However, I prefer a more deliberate approach. Instead of having abbrevs expand automatically, I like to trigger them manually with the command expand-abbrev, which is bound to C-x '. This gives me full control and avoids unintentional expansions. If you prefer the automatic method, you can always enable it with M-x abbrev-mode.

Level 1: Simple Text Replacements

Let's start with the basics. The simplest use of abbrevs is to replace a short string with a longer one. This is perfect for things you type often, like symbols, emojis, or special characters.

Here is a minimal configuration example (full nicer code on the end of this post):

(use-package abbrev
  :ensure nil
  :custom
  (save-abbrevs nil)
  :config
  (define-abbrev-table 'global-abbrev-table
	'(;; Arrows
	  ("ra" "β†’")
	  ("la" "←")
	  ("ua" "↑")
	  ("da" "↓")

	  ;; Emojis for context markers
	  ("todo"  "πŸ‘· TODO:")
	  ("fixme" "πŸ”₯ FIXME:")
	  ("note"  "πŸ“Ž NOTE:")
	  ("hack"  "πŸ‘Ύ HACK:")
	  ("smile"  "πŸ˜„")
	  ("party" "πŸŽ‰")
	  ("up"  "☝️")
	  ("applause" "πŸ‘")
	  ("manyapplauses" "πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘")
	  ("heart" "❀️")

	  ;; NerdFonts
	  ("nerdfolder" " ")
	  ("nerdgit" "ξœ‚")
	  ("nerdemacs" ""))))

With these defined, I can type ra and then C-x ' to get a β†’, or fixme followed by C-x ' to get "πŸ”₯ FIXME:". It's simple, fast, and incredibly useful.

abbrev-mode-01

Level 2: Running Functions

What if you need more than just static text? What if you want to position the cursor in a specific spot after the expansion? Abbrevs can do that too, by executing an Emacs Lisp function as part of the expansion.

Let's look at a common use case: creating a Markdown code block and placing the cursor inside it.

;; Markdown
("cb" "```@\n\n```"
 (lambda () (search-backward "@") (delete-char 1)))

;; ORG
("ocb" "#+BEGIN_SRC @\n\n#+END_SRC"
 (lambda () (search-backward "@") (delete-char 1)))

When I expand cb, it first inserts the string Λ‹Λ‹Λ‹ @\n\n\Λ‹Λ‹Λ‹ . Then, it immediately runs the provided lambda function, which searches backward for the temporary @ marker, deletes it, and leaves the point (the cursor) right where I need it, ready to type. Or mabe we want to do the same for org-mode? No problem, just check ocb.

abbrev-mode-02

You could also make variations of these examples by adding your most used language to the template, maybe adding comments, and whathever else you need!

Level 3: The Big Guns - Interactive Templates

Now for the really powerful stuff. Simple text replacement is great, and cursor positioning is even better, but what about true, interactive templates with placeholders? With a simple helper function (check for emacs-solo/abbrev--replace-placeholders on the full code on the bottom of this post), we can make abbrev-mode do just that.

The core of this system is a function that searches for placeholders like ###1###, ###2###, and a special cursor marker ###@###. It prompts you in the minibuffer for the value of each placeholder and, at the end, places the cursor exactly where you told it to.

Here are a few examples that use this function:

;; part of the `global-abbrev-table'
;;
;; JS/TS snippets
("imp" "import { ###1### } from '###2###';"
 emacs-solo/abbrev--replace-placeholders)
("fn" "function ###1### () {\n ###@### ;\n};"
 emacs-solo/abbrev--replace-placeholders)
("clog" "console.log(\">>> LOG:\", {###@### })"
 emacs-solo/abbrev--replace-placeholders)
("cwarn" "console.warn(\">>> WARN:\", {###@### })"
 emacs-solo/abbrev--replace-placeholders)
("cerr" "console.error(\">>> ERR:\", {###@### })"
 emacs-solo/abbrev--replace-placeholders)

When I expand rfc with C-x ', Emacs first asks me for the value of ###1### (the component name). Then it asks for ###2### (the initial content). It replaces the placeholders with my input and creates the full component structure in seconds.

abbrev-mode-03

The Complete Configuration

As promised, here is the full use-package declaration that powers this system. You can copy and paste this directly into your Emacs configuration to get started.

(use-package abbrev
  :ensure nil
  :custom
  (save-abbrevs nil)
  :config
  (defun emacs-solo/abbrev--replace-placeholders ()
	"Replace placeholders ###1###, ###2###, ... with minibuffer input.
If ###@### is found, remove it and place point there at the end."
	(let ((cursor-pos nil))
	  (save-excursion
		(goto-char (point-min))
		(let ((loop 0)
			  (values (make-hash-table :test 'equal)))
		  (while (re-search-forward "###\\([0-9]+\\|@\\)###" nil t)
			(setq loop (1+ loop))
			(let* ((index (match-string 1))
				   (start (match-beginning 0))
				   (end (match-end 0)))
			  (cond
			   ((string= index "@")
				(setq cursor-pos start)
				(delete-region start end))
			   (t
				(let* ((key (format "###%s###" index))
					   (val (or (gethash key values)
								(let ((input (read-string (format "Value for %s: " key))))
								  (puthash key input values)
								  input))))
				  (goto-char start)
				  (delete-region start end)
				  (insert val)
				  (goto-char (+ start (length val))))))))))
	  (when cursor-pos
		(goto-char cursor-pos))))

  (define-abbrev-table 'global-abbrev-table
	'(;; Arrows
	  ("ra" "β†’")
	  ("la" "←")
	  ("ua" "↑")
	  ("da" "↓")

	  ;; Emojis for context markers
	  ("todo"  "πŸ‘· TODO:")
	  ("fixme" "πŸ”₯ FIXME:")
	  ("note"  "πŸ“Ž NOTE:")
	  ("hack"  "πŸ‘Ύ HACK:")
	  ("pinch"  "🀌")
	  ("smile"  "πŸ˜„")
	  ("party" "πŸŽ‰")
	  ("up"  "☝️")
	  ("applause" "πŸ‘")
	  ("manyapplauses" "πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘πŸ‘")
	  ("heart" "❀️")

	  ;; NerdFonts
	  ("nerdfolder" " ")
	  ("nerdgit" "ξœ‚")
	  ("nerdemacs" "")

	  ;; HTML entities
	  ("nb" " ")
	  ("lt" "<")
	  ("gt" ">")

	  ;; Markdown
	  ("cb" "```@\n\n```"
	   (lambda () (search-backward "@") (delete-char 1)))

	  ;; ORG
	  ("ocb" "#+BEGIN_SRC @\n\n#+END_SRC"
	   (lambda () (search-backward "@") (delete-char 1)))
	  ("oheader" "#+TITLE: ###1###\n#+AUTHOR: ###2###\n#+EMAIL: ###3###\n#+OPTIONS: toc:nil\n"
	   emacs-solo/abbrev--replace-placeholders)

	  ;; JS/TS snippets
	  ("imp" "import { ###1### } from '###2###';"
	   emacs-solo/abbrev--replace-placeholders)
	  ("fn" "function ###1### () {\n ###@### ;\n};"
	   emacs-solo/abbrev--replace-placeholders)
	  ("clog" "console.log(\">>> LOG:\", { ###@### })"
	   emacs-solo/abbrev--replace-placeholders)
	  ("cwarn" "console.warn(\">>> WARN:\", { ###@### })"
	   emacs-solo/abbrev--replace-placeholders)
	  ("cerr" "console.error(\">>> ERR:\", { ###@### })"
	   emacs-solo/abbrev--replace-placeholders)
	  ("afn" "async function() {\n  \n}"
	   (lambda () (search-backward "}") (forward-line -1) (end-of-line)))
	  ("ife" "(function() {\n  \n})();"
	   (lambda () (search-backward ")();") (forward-line -1) (end-of-line)))
	  ("esdeps" "// eslint-disable-next-line react-hooks/exhaustive-deps"
	   (lambda () (search-backward ")();") (forward-line -1) (end-of-line)))
	  ("eshooks" "// eslint-disable-next-line react-hooks/rules-of-hooks"
	   (lambda () (search-backward ")();") (forward-line -1) (end-of-line)))

	  ;; React/JSX
	  ("rfc" "const ###1### = () => {\n  return (\n    <div>###2###</div>\n  );\n};"
	   emacs-solo/abbrev--replace-placeholders))))

Conclusion

abbrev-mode may not be the flashiest tool in the Emacs ecosystem, but it has something special: it’s yours to shape. With just a handful of lines of Elisp, you can transform it from a humble text expander into a personal snippet engine that’s fast, predictable, and tailored to the way you think and write.

By starting small, simple replacements and handy symbols and gradually layering in cursor control, minibuffer-driven placeholders, and dynamic templates, you build a system that grows with your workflow. No external dependencies, no syncing snippet collections across machines, no black-box magic. Just pure Emacs, bending to your will.

Whether you stick with this approach or eventually reach for heavier tools like yasnippet or tempel, I hope this post shows that sometimes the most powerful solutions are the ones hiding in plain sight. And with a little crafting, abbrev-mode can become one of the most quietly transformative parts of your everyday editing.

Happy hacking. And happy expanding!