# leaf.el is a clean-room use-package for Emacs configuration

> leaf.el is a declarative package configuration macro for Emacs, written from scratch by an author who had spent two and a half years fighting with use-package. Its keyword vocabulary is nearly identical to use-package's, the differences are documented one by one, and the keyword implementations live in a separate repository so they can be added without touching the core.

**conao3/leaf.el** — Flexible, declarative, and modern init.el package configuration

- Repository: https://github.com/conao3/leaf.el
- Stars: 556 · Forks: 27
- Language: Emacs Lisp
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/conao3-leaf-el

## Written from scratch after two and a half years

The description of leaf.el is short and honest about what it is. It is yet another use-package.

What follows is the reason it exists. The author solved the stress he felt while using use-package for two and a half years, and by developing from scratch the result is a cleaner and more predictable implementation than use-package.

That is the whole positioning, and it is a specific and unusual one. This is not a wrapper, a fork with a few changes, or a compatibility layer. It is a reimplementation with the same interface, written by someone who had already spent years living with the original and knew exactly where it hurt.

The practical consequence is stated in the next line. It makes it easy to maintain and add new keywords, and the place where that happens is a separate file, leaf-keywords.el, which lives in its own repository.

That separation is the design decision with the longest reach. Splitting the keyword implementations out means the core of leaf.el stays small and the growing surface lives somewhere else. A new keyword is a new function in that file rather than a change to the core macro.

The other framing point is that leaf has keywords almost identical to use-package, but some of the usage of the keywords is different. So for someone arriving from use-package, the vocabulary is familiar and the semantics are not always.

The project is Emacs Lisp, GPL-3.0 licensed, with the default branch master and the last push on 2026-05-08. There are no GitHub releases, and the package is distributed through MELPA and stable MELPA, both of which have badges in the header.

## The keyword list is the API

The documentation leads with the available keywords, and the call to list them is itself part of the interface.

```emacs-lisp
(leaf-available-keywords)
;;=> (:disabled
;;    :leaf-protect
;;    :load-path :load-path*
;;    :leaf-autoload
;;    :doc :req :tag :file :url
;;    :defun :defvar :leaf-defun :leaf-defvar
;;    :preface
;;    :when :unless :if
;;    :emacs< :emacs<= :emacs= :emacs> :emacs>=
;;    :ensure :package :vc :feather :straight :el-get
;;    :after
;;    :commands
;;    :bind :bind*
;;    :mode :interpreter :magic :magic-fallback
;;    :hook
;;    :advice :advice-remove
;;    :init
;;    :pre-setq :pl-pre-setq :auth-pre-setq
;;    :custom :custom* :pl-custom :auth-custom :custom-face
;;    :require
;;    :hydra :transient :combo :combo*
;;    :smartrep :smartrep* :chord :chord*
;;    :mode-hook
;;    :leaf-defer
;;    :config
;;    :diminish :delight
;;    :global-minor-mode
;;    :setq :setq-default
;;    :pl-setq :auth-setq :pl-setq-default :auth-setq-default)
```

Reading that list tells you what the package can do and, more usefully, how the vocabulary is organised.

There are the package acquisition keywords: require, package, ensure and vc. There is a separate set for the newer package managers: feather, straight and el-get. There are the loading and deferral keywords: load-path, load-path*, leaf-autoload, leaf-defer and leaf-protect.

Then the configuration keywords, which is where most of the vocabulary lives. There is custom, custom*, custom-face and pl-custom for user options. There is pre-setq, setq and setq-default for variables, and pl-setq and auth-setq for the two special cases of those, where pl and auth are the prefixes for package-local and authentication variables.

There is setf, push, pre-setf and pre-push for the modifying forms. There is defun, defvar, leaf-defun and leaf-defvar. And there are the hooks and conditions: hook, mode-hook, when, unless, if, disabled, and the version comparisons emacs<, emacs<=, emacs=, emacs> and emacs>=.

The interaction keywords are there too: bind, bind*, bind-keymap, bind-keymap*, commands and after. And the keybinding-building keywords: mode, interpreter, magic and magic-fallback.

That is a very large set of keywords, and the list is the honest measure of the surface area. It is large enough that predictability matters, which is exactly what the author was trying to buy.

The documentation also gives a debugging technique, which is the most practical thing in it. The quickest way to solve a problem is using macroexpand-1 to see the unfolded result if it is not what you intended. And there are a number of samples in the documentation and more in the test file, leaf-tests.el.

## Installing it, and the two packages it was meant to replace

The installation path is package.el from MELPA, and the code goes at the top of your init.el.

```emacs-lisp
;; <leaf-install-code>
(eval-and-compile
  (customize-set-variable
   'package-archives '(("org" . "https://orgmode.org/elpa/")
                       ("melpa" . "https://melpa.org/packages/")
                       ("gnu" . "https://elpa.gnu.org/packages/")))
  (package-initialize)
  (unless (package-installed-p 'leaf)
    (package-refresh-contents)
    (package-install 'leaf))

  (leaf leaf-keywords
    :ensure t
    :init
```

The shape is conventional for an Emacs package: set up the package archives, initialize package.el, refresh the contents, install leaf if it is not already there, and then declare leaf itself as a package with leaf-keywords as an:ensure dependency. The optional packages follow, commented as being for the features you want to use such as:hydra,:el-get or:blackout.

The archive list is the standard one, with orgmode.org, MELPA and GNU ELPA. Both the MELPA and stable MELPA badges are in the header, so both channels are supported.

There are two adjacent packages that are worth understanding because the documentation records their history explicitly.

The first is feather.el, which was intended to be developed as a replacement for package.el. The status line says it was achieved, but adds the caveat that it is now available and is just a package.el wrapper. So feather exists, but the replacement never became one.

The second is leaf-key.el, which was intended to replace bind-key. That one was also achieved, and here the note says Now leaf builtin. So the keybinding functionality that would have lived in a separate package is part of leaf itself, and the keyword list reflects that with the bind-keymap and bind-keymap* entries.

Both of these are the kind of detail that normally only shows up in a changelog, and having them stated plainly tells you what the package's relationship is with the rest of the author's toolset.

The project also maintains a Slack channel, a Twitter account, a Patreon page and a Codacy grade badge, and there is a CONTRIBUTING.org and a Makefile at the repository root.

## The test matrix spans six Emacs versions in Docker

The Makefile is where the engineering shows, and it is more thorough than most Emacs packages manage.

The matrix is defined in two lines. The Ubuntu versions are 24.1, 24.5 and 25.1. The Alpine versions are 25.3, 26.1 and 26.2. And the Docker image set is built from both lists, with the ubuntu-min and alpine-min tags.

So the test suite runs against six Emacs versions across two Linux distributions, which covers both the old stable series and the newest ones. For a package whose whole purpose is to expand into a large set of macros and to behave predictably, that spread matters, because a keyword that works on one Emacs and misexpands on another is exactly the kind of bug this package is trying to avoid.

The mechanics are containerised. The check target builds the byte-compiled files and then runs the test file in batch mode with a specific test runner, and the allcheck target spins up one container per Emacs version using the conao3/emacs images, copies the working tree into the container, runs the tests inside it, and pipes the output through tee into a per-version log file that the target then cats and greps for the separator.

There is a silent variant of the same job for continuous integration, and the difference between the two is only whether the output is shown.

Byte compilation is part of the build, with the pattern rule compiling each .el file to .elc through a batch byte-compile invocation using a clean emacs with -Q and no user configuration. The batch line is emacs -Q --batch -L with the top-level directory on the load path.

The test files are leaf-tests.el and cort-test.el, and the Makefile lists the .el files that are compiled as ELS. There is also a git-hook target that copies the hooks from the git-hooks directory into .git/hooks, which means the repository ships its own commit hooks and installs them for you on make all.

## Migration is a first-class part of the design

The table of contents lists three migration sections, and their presence says something about the project's expectations.

There is a leaf v3.0 to v4.0 section, a leaf v2.0 to v3.0 section and a leaf v1.0 to v2.0 section. Three major version transitions are documented in the same document as the keyword reference, which means the author expects people to arrive from an older version of the same package and wants the upgrade to be a guided change rather than a hunt through the diff.

That expectation is a reasonable one. A package that expands into so many keywords is not something you can read at a glance, so a version upgrade is exactly the situation where you need a document that says which keywords changed meaning.

The pattern of the keyword names is also a versioning story in itself. Several keywords carry a prefix that marks them as particular to a package manager or a convention: pl- prefixed keywords such as pl-setq, pl-custom and pl-pre-setq, and auth- prefixed keywords such as auth-pre-setq and auth-setq-default. The leaf-prefixed keywords, leaf-protect, leaf-defer, leaf-autoload and leaf-defun, are the ones the package defines itself rather than borrowing from use-package.

That prefix discipline is what makes the keyword list readable. When you see pl- or auth- you know you are in a specific convention, and when you see leaf- you know you are in the package's own extension rather than in a borrowed concept.

There is also a documentation set with:doc,:req,:tag,:file and:url keywords, which means the package can carry the documentation metadata for the packages it declares, which is how the MELPA entry and the in-Emacs documentation get built.

## Where this sits against use-package and against plain require

The comparison with use-package is the one the project itself makes, and the interesting question is when the rewrite is worth it.

Anything you can configure with use-package, you can probably configure with leaf, since the vocabulary is nearly identical.

The case for leaf is the one the author makes: a cleaner and more predictable implementation, developed from scratch, with keywords maintained in a separate repository so that adding one is a routine job rather than an invasive change. The keyword differences are the practical test of that claim. If a keyword in leaf does what you expected, the reimplementation has done its job, and if it does something subtly different, the documentation tells you so.

The other comparison is with plain require plus a load of setq and add-hook calls, which is what use-package was itself an improvement on. Leaf is not competing with that. A declarative macro that defers loading, groups configuration and supports conditional setup is a real improvement over the manual form, and leaf offers it.

What leaf does not add is anything new in capability. Reading the keyword list, there is no feature here that use-package does not have. The value is in predictability and in maintainability, which are real but hard to value in advance.

So the honest way to decide is to look at your own configuration. If your use-package setup has been stable for years and you understand it, leaf is a lateral move with a migration cost. If you have ever been surprised by what use-package expanded to, or you have wanted to add a keyword and found the source hard to work with, then leaf addresses a real frustration.

The debugging advice matters for the trial. macroexpand-1 on a leaf form shows you the expanded result, so you can see exactly what the macro did rather than guessing, and the test file has more examples than the documentation does.

## Conclusion

Adopt leaf.el if you configure Emacs with use-package and have found its behaviour surprising or hard to extend, since leaf keeps the same vocabulary with a more predictable implementation. Skip it if your setup is working, since the migration path exists and the keyword differences are the thing to read first. Verify first which Emacs versions you support, since the test matrix covers a wide range and the keyword set changes between major releases, that you read the migration notes for the versions you are moving from, and how you want to inspect the expansion, since macroexpand-1 is the recommended debugging tool.

## FAQ

### What is leaf.el and how is it different from use-package?

leaf.el is yet another use-package, written from scratch by an author who had spent two and a half years using use-package and found it stressful. The result is a cleaner and more predictable implementation, with keyword implementations kept in a separate leaf-keywords.el repository so new keywords are easy to add. The keywords are almost identical to use-package, but some of their usage differs, and the documentation includes migration guides for v1 to v2, v2 to v3 and v3 to v4.

### How do I install leaf.el?

leaf.el and leaf-keywords.el install with package.el from MELPA. Put the install code at the top of your init.el: set package-archives to orgmode.org ELPA, MELPA and GNU ELPA, call package-initialize, refresh contents and install leaf if it is not present, then declare leaf itself with leaf-keywords as an :ensure dependency. Optional packages such as hydra, el-get and blackout are declared the same way if you want those features.

### Which Emacs versions is leaf.el tested against?

The Makefile defines the matrix as Ubuntu versions 24.1, 24.5 and 25.1 and Alpine versions 25.3, 26.1 and 26.2, with the Docker image set built from both lists. The allcheck target runs the test file in a container per version using the conao3/emacs images, and the byte compilation is done with a clean emacs -Q --batch invocation. The recommended way to see what a keyword expands to is macroexpand-1.

## Sources

- [conao3/leaf.el on GitHub](https://github.com/conao3/leaf.el)
- [Issues](https://github.com/conao3/leaf.el/issues)
- [License: GPL-3.0](https://github.com/conao3/leaf.el/blob/master/LICENSE)
- [README](https://github.com/conao3/leaf.el/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/conao3-leaf-el
