| name | emacs-transient |
| description | Expert guide for building Emacs transient menus (keyboard-driven UI like Magit). Use when implementing or debugging transient-define-prefix, transient-define-suffix, or transient-define-infix commands. |
Emacs Transient Expert
This skill provides comprehensive guidance for building transient-based interfaces in Emacs Lisp, based on the official transient library and real-world implementations in Magit, Forge, and transient-showcase.
When to Use This Skill
Invoke this skill when:
- Implementing new
transient-define-prefixmenus - Creating
transient-define-suffixortransient-define-infixcommands - Debugging transient menu layouts or behavior
- Understanding transient levels, groups, or conditional display
- Working on keyboard-driven UI similar to Magit
- Converting traditional Emacs commands to transient interfaces
Core Concepts
What is Transient?
Transient is a library for creating keyboard-driven, temporary menus in Emacs. It's the foundation of Magit's interface and provides:
- Display of current state - Show active arguments and options
- Modal bindings - Temporary keymaps that disappear when done
- Contextual UI - Menus adapt based on state
- Persistence - Save and restore argument values across sessions
- History - Track previously used values
Key Terminology
- Prefix - The main transient command that opens the menu
- Suffix - Commands invoked from the transient (actions)
- Infix - Special suffixes that set arguments/options without exiting
- Groups - Organizational units for layout (rows, columns, sections)
- Levels (1-7) - Control visibility based on user expertise (default: 4)
- Scope - Contextual value passed to suffixes (e.g., current branch)
Three Core Macros
1. transient-define-prefix
Defines the main transient menu.
(transient-define-prefix my-menu ()
"Description of what this menu does."
:man-page "git-commit" ; Optional: link to man page
:info-manual "(magit)Committing" ; Optional: link to info manual
:value '("--verbose") ; Optional: default arguments
;; Groups define layout
["Arguments" ; Group header
("-v" "Verbose" "--verbose") ; Switch (toggle)
("-a" "Author" "--author=" ; Option (takes value)
:prompt "Author: ")
(my-custom-infix)] ; Reference to defined infix
[["Actions" ; Nested groups = columns
("c" "Commit" my-commit-cmd)
("a" "Amend" my-amend-cmd)]
["Other"
("q" "Quit" transient-quit)]])
Key slots:
:value- Default arguments:man-page- Man page for help:info-manual- Info manual section:transient-suffix- Default transient behavior for suffixes:transient-non-suffix- Allow/forbid non-suffix commands:refresh-suffixes- When to refresh suffix state
2. transient-define-suffix
Defines action commands (suffixes).
(transient-define-suffix my-commit-cmd (args)
"Create a commit with ARGS."
:description "Commit staged changes" ; Optional: override in menu
:transient t ; Stay transient after calling
(interactive (list (transient-args 'my-menu)))
(apply #'my-run-git "commit" args))
Key slots:
:key- Key binding (can override menu binding):description- Can be string or function returning string:transient- Control transient state (see below):if/:if-not- Conditional visibility:inapt-if/:inapt-if-not- Show but disable
3. transient-define-infix (or transient-define-argument)
Defines argument commands (infixes).
(transient-define-argument my-author-arg ()
:description "Set author"
:class 'transient-option ; Option class (takes value)
:shortarg "-a" ; Short form
:argument "--author=" ; Long form
:reader #'my-read-author) ; Custom reader function
(transient-define-infix my-verbose-switch ()
:description "Verbose output"
:class 'transient-switch ; Switch class (boolean)
:argument "--verbose")
Infix/Suffix Classes
Suffix Classes
transient-suffix- Base class for all suffixestransient-infix- Base for all infixes (auto-stays transient)
Infix Classes (derive from transient-infix)
For command-line arguments:
transient-switch- Boolean flag (e.g.,--verbose)transient-option- Argument with value (e.g.,--author=NAME)transient-switches- Mutually exclusive optionstransient-files- File arguments (--separator)
For variables:
transient-variable- Base for variable infixestransient-lisp-variable- Set Emacs Lisp variables
Display only:
transient-information- Display info (no command/key)transient-information*- Info aligned with descriptions
Suffix Specification Syntax
Three ways to specify suffixes:
1. Inline (shorthand)
("key" "description" command)
("-s" "switch" "--switch") ; Auto-creates transient-switch
("-o" "option" "--option=") ; Auto-creates transient-option
2. With keyword arguments
("key" "description" command
:transient t ; Stay transient
:if (lambda () (my-condition))) ; Conditional
("-a" "author" "--author="
:prompt "Author name: "
:reader #'my-custom-reader
:always-read t) ; Always prompt, don't toggle
3. Reference to separately defined command
(my-custom-suffix) ; Uses suffix's own key/desc
Group Specification Syntax
[{LEVEL} {DESCRIPTION} {KEYWORD VALUE}... ELEMENT...]
Common patterns:
["Group Title" ; Simple group with title
("k" "desc" cmd)]
[:description "Dynamic" ; Dynamic description
:description (lambda () (format "Time: %s" (current-time-string)))
("k" "desc" cmd)]
[:if some-predicate ; Conditional group
("k" "desc" cmd)]
[:class transient-row ; Explicit layout class
("k" "desc" cmd)]
[["Column 1" ; Nested groups = columns
("a" "cmd a" cmd-a)]
["Column 2"
("b" "cmd b" cmd-b)]]
Group classes:
transient-column- Stack items vertically (default)transient-row- Arrange items horizontallytransient-columns- Contains column groups side-by-sidetransient-subgroups- Contains subgroups
The :transient Slot (Controlling State)
Controls whether transient stays active after invoking a command.
For suffixes (default: exit transient):
nilor:transient nil- Exit transient (default for suffixes)tor:transient t- Stay transient:transient 'transient--do-call- Export args and stay:transient 'transient--do-return- Return to parent prefix
For infixes (default: stay transient):
- Infixes always use
transient--do-stayby default - Rarely need to override
For sub-prefixes (nested transients):
nil- Exit all transients when sub-prefix exitst- Return to parent when sub-prefix exits:transient 'transient--do-recurse- Enable return behavior:transient 'transient--do-replace- Replace parent (no return)
Common pre-commands:
transient--do-exit- Exit and export argstransient--do-stay- Stay, don't export argstransient--do-call- Stay and export argstransient--do-return- Exit to parent prefix
Layout Patterns
Stacked Groups (Vertical)
(transient-define-prefix my-menu ()
["Top Group" ...]
["Bottom Group" ...])
Columns (Side-by-side)
(transient-define-prefix my-menu ()
[["Left Column" ...]
["Right Column" ...]])
Mixed (Stacked + Columns)
(transient-define-prefix my-menu ()
["Top Group (full width)" ...]
[["Left Column" ...]
["Right Column" ...]])
Grid Layout
(transient-define-prefix my-menu ()
[:description "The Grid"
["Left Column"
("tl" "top-left" cmd)
("bl" "bottom-left" cmd)]
["Right Column"
("tr" "top-right" cmd)
("br" "bottom-right" cmd)]])
Spacing
["Group"
"" ; Empty line
("k" "first" cmd)
("l" "second" cmd)
"" ; Another gap
("m" "third" cmd)]
Accessing Transient Values
In suffix commands
(transient-define-suffix my-suffix (args)
"Do something with ARGS."
(interactive (list (transient-args 'my-prefix)))
;; Now use args...
(message "Args: %S" args))
Getting specific argument values
(let* ((args (transient-args 'my-prefix))
(author (transient-arg-value "--author=" args))
(verbose-p (transient-arg-value "--verbose" args)))
;; Use values...
)
Using scope (contextual value)
(transient-define-prefix my-menu (scope)
"Menu with scope."
["Actions"
("a" "Action" my-action)]
(interactive "P") ; Can take prefix arg as scope
(transient-setup 'my-menu nil nil :scope scope))
(transient-define-suffix my-action ()
(interactive)
(let ((scope (transient-scope)))
(message "Scope: %S" scope)))
Conditional Display (Predicates)
Visibility predicates (if suffix should appear)
("k" "desc" cmd
:if (lambda () (file-exists-p "Makefile"))) ; Show if true
("k" "desc" cmd
:if-not some-mode ; Show if not in mode
:if-non-nil some-variable ; Show if var non-nil
:if-mode 'emacs-lisp-mode ; Show in mode
:if-derived 'prog-mode) ; Show if derived
Inapt predicates (show but grayed out)
("k" "desc" cmd
:inapt-if (lambda () (not (magit-anything-staged-p)))) ; Gray if true
("k" "desc" cmd
:inapt-if-not some-function ; Gray if false
:inapt-if-nil some-variable) ; Gray if var nil
On groups
[:if magit-rebase-in-progress-p ; Whole group conditional
("a" "abort" magit-rebase-abort)
("c" "continue" magit-rebase-continue)]
Important Suffix Slots
Required/Common:
:key- Key binding:description- String or function returning string:command- The command to invoke
Behavioral:
:transient- Stay transient? (t/nil/pre-command):level- Visibility level (1-7, default 4)
Conditional:
:if,:if-not,:if-mode,:if-derived- Visibility:inapt-if,:inapt-if-not- Enable/disable
Display:
:format- Custom display format (%k%d%v):face- Face for description:summary- Echo area/tooltip text
Help:
:show-help- Custom help function
Important Infix Slots
All suffix slots, plus:
Argument-related:
:argument- Long form (e.g.,--verbose):shortarg- Short form (e.g.,-v):class- Infix class (switch/option/etc.)
Reading values:
:reader- Function to read value (PROMPT, INITIAL, HISTORY):prompt- Prompt string or function:choices- List of valid values:always-read- Always prompt (don't toggle for options):allow-empty- Allow empty string
Multi-value:
:multi-value-'restor'repeatfor multiple values
Other:
:init-value- Function to set initial value:history-key- Symbol for history (share across infixes):unsavable- Don't save with prefix value
Levels (1-7)
Control visibility based on user preference:
- 1-3: Essential commands (always visible)
- 4: Default level
- 5-6: Advanced/rarely used
- 7: Experimental/debug
- 0: Never show (effectively disabled)
("k" "advanced" cmd :level 6) ; Only show at level 6+
["Arguments" :level 5 ; Whole group at level 5
("-v" "verbose" "--verbose")]
Users can change levels interactively with C-x l.
Dynamic Content
Dynamic descriptions
("k" my-cmd
:description (lambda ()
(format "Branch: %s" (magit-get-current-branch))))
Dynamic groups (:setup-children)
[:class transient-column
:setup-children
(lambda (_)
(transient-parse-suffixes
'my-prefix
(mapcar (lambda (file)
(list (substring file 0 1) file
(lambda () (interactive) (find-file file))))
(directory-files "."))))]
Information display
["Info"
(:info "Static information")
(:info (lambda () (format "Dynamic: %s" (current-time-string))))
(:info my-info-function :format " %d")] ; Custom format
Common Patterns from Magit
Context-aware suffixes
(transient-define-suffix my-cmd (args)
(interactive
(if (derived-mode-p 'my-list-mode)
(list (my-get-args-from-buffer))
(list (transient-args 'my-prefix))))
...)
Shared argument groups
(transient-define-group my-common-args ()
["Common Arguments"
("-v" "Verbose" "--verbose")
("-q" "Quiet" "--quiet")])
(transient-define-prefix my-menu-1 ()
[my-common-args] ; Include by reference
["Actions" ...])
(transient-define-prefix my-menu-2 ()
[my-common-args] ; Reuse in another menu
["Other Actions" ...])
Validation before execution
(transient-define-suffix my-create (title desc)
(interactive
(list (read-string "Title: ")
(read-string "Description: ")))
(when (string-empty-p title)
(user-error "Title cannot be empty"))
(my-create-thing title desc))
Best Practices
1. Use appropriate classes
- Use
transient-switchfor boolean flags - Use
transient-optionfor value-taking arguments - Use
transient-switchesfor mutually exclusive options
2. Leverage levels effectively
- Put common operations at level 3-4
- Put advanced features at level 5-6
- Use level 7 for debug/experimental
3. Provide good descriptions
- Keep descriptions concise (fits in menu)
- Use dynamic descriptions to show state
- Use
:summaryfor longer explanations
4. Handle state properly
- Use
:transient tfor commands that should stay in menu - Use
:ifpredicates instead of manual state checking - Use
:inapt-ifto show unavailable options
5. Organize logically
- Group related items together
- Use columns for parallel choices
- Put common actions on left, advanced on right
6. Share history
- Use
:history-keyto share history between similar infixes
(transient-define-argument my-author ()
:argument "--author="
:history-key 'my-package-author-history)
7. Provide help
- Set
:man-pageor:info-manualon prefix - Use descriptive docstrings
- Implement custom
:show-helpif needed
8. Test interactively
- Use
C-hwhile transient is active to see help - Use
C-x lto experiment with levels - Use
C-x s/C-x C-sto test persistence
Common Gotchas
Don't quote in transient definitions - The macro handles it
;; WRONG: ["Group" '("k" "desc" 'cmd)] ;; RIGHT: ["Group" ("k" "desc" cmd)]Use :transient t for iterative commands
("n" "next" my-next :transient t) ; Can press 'n' repeatedlyRemember infixes stay transient by default
- Don't need
:transient ton infixes - They automatically use
transient--do-stay
- Don't need
:if vs :inapt-if
:if- completely hide the suffix:inapt-if- show but grayed out
Accessing args in interactive
(interactive (list (transient-args 'my-prefix))) ; CorrectSub-prefix returns
("s" "sub-menu" my-sub-prefix :transient t) ; Returns to parent
Complete Example
;; Custom argument
(transient-define-argument my-pkg:--author ()
:description "Override author"
:class 'transient-option
:shortarg "-a"
:argument "--author="
:reader #'my-read-author)
;; Suffix that stays transient
(transient-define-suffix my-pkg-preview ()
"Preview current settings."
:transient t
(interactive)
(message "Args: %S" (transient-args 'my-pkg-create)))
;; Main suffix
(transient-define-suffix my-pkg-execute (args)
"Execute with ARGS."
(interactive (list (transient-args 'my-pkg-create)))
(apply #'my-pkg-run args))
;; Main menu
(transient-define-prefix my-pkg-create ()
"Create something with options."
:man-page "my-tool"
:value '("--verbose")
["Arguments"
("-v" "Verbose" "--verbose")
("-q" "Quiet" "--quiet")
("-n" "Dry run" "--dry-run" :level 5)
(my-pkg:--author)]
[["Actions"
("p" "Preview" my-pkg-preview)
("c" "Create" my-pkg-execute)]
["Other"
("q" "Quit" transient-quit)]])
Reference Materials
For deeper understanding, refer to:
- transient-reference/transient/ - Core library documentation
- transient-reference/magit/ - Real-world usage in Magit
- transient-reference/forge/ - Additional patterns in Forge
- transient-reference/transient-showcase/ - Examples showcase
Version History
- v1.0.0 (2025-11-06): Initial skill created from research of transient, Magit, Forge, and transient-showcase repositories