Crafting Your Own Snippets with Emacs Built-In Abbrev Mode

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.

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.

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.

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!