widget
A guide to using the Emacs Widget Library for creating interactive UI elements in buffers.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install hugoduncan-library-skills-widget
Repository
Skill path: plugins/emacs-libraries/skills/widget
A guide to using the Emacs Widget Library for creating interactive UI elements in buffers.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Frontend.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: hugoduncan.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install widget into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/hugoduncan/library-skills before adding widget to shared team environments
- Use widget for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: widget
description: A guide to using the Emacs Widget Library for creating interactive UI elements in buffers.
---
# Emacs Widget Library
Build interactive forms and UI elements in Emacs buffers using the built-in widget library (wid-edit.el).
## Overview
The Emacs Widget Library provides a framework for creating interactive UI elements within text buffers. Widgets enable building forms, configuration interfaces, and custom UI components without requiring external dependencies.
**Core libraries:**
- `widget.el` - Top-level widget interface
- `wid-edit.el` - Widget implementation and editing
**Primary use cases:**
- Custom configuration interfaces
- Interactive forms and dialogs
- Settings and preference editors
- Data entry and validation
## Setup
```elisp
(require 'widget)
(eval-when-compile
(require 'wid-edit))
```
## Core Concepts
### Widget Lifecycle
1. **Create** - `widget-create` returns a widget object
2. **Setup** - `widget-setup` enables interaction after creation
3. **Interact** - User edits/activates widgets
4. **Query** - Retrieve values with `widget-value`
5. **Delete** - Clean up with `widget-delete`
### Widget Buffer Setup
```elisp
(defun my-widget-example ()
(interactive)
(switch-to-buffer "*Widget Example*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
;; Create widgets
(widget-insert "Example Form\n\n")
(widget-create 'editable-field
:format "Name: %v"
:size 30
"Enter name")
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Submitted!"))
"Submit")
(widget-insert "\n")
;; Enable widgets
(use-local-map widget-keymap)
(widget-setup))
```
### Widget Properties
Widgets are configured via keyword arguments:
- `:value` - Initial/current value
- `:format` - Display format string
- `:tag` - Label text
- `:notify` - Callback function on change
- `:help-echo` - Tooltip text
## Widget Types
### Text Input
#### editable-field
Basic text input field.
```elisp
;; Simple field
(widget-create 'editable-field
:format "Label: %v\n"
"default text")
;; Sized field
(widget-create 'editable-field
:size 20
:format "Username: %v\n")
;; Password field
(widget-create 'editable-field
:secret ?*
:format "Password: %v\n")
;; With change notification
(widget-create 'editable-field
:notify (lambda (widget &rest ignore)
(message "Value: %s" (widget-value widget)))
:format "Email: %v\n")
```
#### text
Multi-line text area.
```elisp
(widget-create 'text
:format "Comments:\n%v"
:value "Line 1\nLine 2\nLine 3")
```
### Buttons
#### push-button
Clickable button that triggers action.
```elisp
;; Basic button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Clicked!"))
"Click Me")
;; Styled button
(widget-create 'push-button
:button-face 'custom-button
:format "%[%t%]\n"
:tag "Submit Form"
:notify (lambda (&rest ignore)
(my-submit-form)))
```
#### link
Hyperlink that executes action.
```elisp
;; Function link
(widget-create 'link
:button-face 'info-xref
:help-echo "View documentation"
:notify (lambda (&rest ignore)
(describe-function 'widget-create))
"Documentation")
;; URL link
(widget-create 'url-link
:format "%[%t%]"
:tag "Emacs Manual"
"https://www.gnu.org/software/emacs/manual/")
```
### Selection
#### checkbox
Boolean toggle (checked/unchecked).
```elisp
(widget-create 'checkbox
:format "%[%v%] Enable feature\n"
:notify (lambda (widget &rest ignore)
(message "Feature %s"
(if (widget-value widget)
"enabled"
"disabled")))
t) ; Initial state
```
#### toggle
Text-based on/off switch.
```elisp
(widget-create 'toggle
:on "Enabled"
:off "Disabled"
:notify (lambda (widget &rest ignore)
(my-update-setting (widget-value widget)))
nil) ; Initially off
```
#### radio-button-choice
Single selection from multiple options.
```elisp
(widget-create 'radio-button-choice
:value "medium"
:notify (lambda (widget &rest ignore)
(message "Selected: %s" (widget-value widget)))
'(item "small")
'(item "medium")
'(item "large"))
```
#### menu-choice
Dropdown menu selection.
```elisp
(widget-create 'menu-choice
:tag "Output Format"
:value 'json
'(const :tag "JSON" json)
'(const :tag "XML" xml)
'(const :tag "Plain Text" text)
'(editable-field :menu-tag "Custom" "custom-format"))
```
#### checklist
Multiple selections (subset of options).
```elisp
(widget-create 'checklist
:notify (lambda (widget &rest ignore)
(message "Selected: %S" (widget-value widget)))
'(const :tag "Option A" option-a)
'(const :tag "Option B" option-b)
'(const :tag "Option C" option-c))
```
### Lists
#### editable-list
Dynamic list with add/remove buttons.
```elisp
(widget-create 'editable-list
:entry-format "%i %d %v"
:value '("item1" "item2")
'(editable-field :value ""))
```
**Format specifiers:**
- `%i` - Insert button (INS)
- `%d` - Delete button (DEL)
- `%v` - The value widget
## Widget API
### Creation and Setup
#### widget-create
Create widget and return widget object.
```elisp
(widget-create TYPE [KEYWORD ARGUMENT]...)
;; Example
(setq my-widget
(widget-create 'editable-field
:size 25
:value "initial"))
```
#### widget-setup
Enable widgets after creation. Must be called before user interaction.
```elisp
(widget-setup)
```
Required after:
- Initial widget creation
- Calling `widget-value-set`
- Modifying widget structure
#### widget-insert
Insert text at point (not a widget).
```elisp
(widget-insert "Header Text\n\n")
```
### Value Access
#### widget-value
Get current widget value.
```elisp
(widget-value WIDGET)
;; Example
(let ((name (widget-value name-widget))
(enabled (widget-value checkbox-widget)))
(message "Name: %s, Enabled: %s" name enabled))
```
#### widget-value-set
Set widget value programmatically.
```elisp
(widget-value-set WIDGET VALUE)
(widget-setup) ; Required after value change
;; Example
(widget-value-set email-widget "[email protected]")
(widget-setup)
```
### Property Access
#### widget-get
Retrieve widget property.
```elisp
(widget-get WIDGET PROPERTY)
;; Example
(widget-get my-widget :tag)
(widget-get my-widget :size)
```
#### widget-put
Set widget property.
```elisp
(widget-put WIDGET PROPERTY VALUE)
;; Example
(widget-put my-widget :help-echo "Enter your email address")
```
#### widget-apply
Call widget method with arguments.
```elisp
(widget-apply WIDGET PROPERTY &rest ARGS)
;; Example
(widget-apply my-widget :notify)
```
### Navigation
#### widget-forward
Move to next widget (bound to TAB).
```elisp
(widget-forward &optional COUNT)
```
#### widget-backward
Move to previous widget (bound to S-TAB/M-TAB).
```elisp
(widget-backward &optional COUNT)
```
### Cleanup
#### widget-delete
Delete widget and clean up.
```elisp
(widget-delete WIDGET)
```
## Common Patterns
### Form with Validation
```elisp
(defun my-registration-form ()
(interactive)
(let (username-widget email-widget)
(switch-to-buffer "*Registration*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Registration\n\n")
(setq username-widget
(widget-create 'editable-field
:format "Username: %v\n"
:size 25))
(widget-insert "\n")
(setq email-widget
(widget-create 'editable-field
:format "Email: %v\n"
:size 40))
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((user (widget-value username-widget))
(email (widget-value email-widget)))
(if (and (> (length user) 0)
(string-match-p "@" email))
(message "Registered: %s <%s>" user email)
(message "Invalid input"))))
"Register")
(use-local-map widget-keymap)
(widget-setup)))
```
### Settings Interface
```elisp
(defun my-settings ()
(interactive)
(let (theme-widget auto-save-widget)
(switch-to-buffer "*Settings*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Application Settings\n\n")
;; Theme selection
(widget-insert "Theme:\n")
(setq theme-widget
(widget-create 'radio-button-choice
:value my-current-theme
'(const :tag "Light" light)
'(const :tag "Dark" dark)
'(const :tag "Auto" auto)))
(widget-insert "\n")
;; Auto-save toggle
(setq auto-save-widget
(widget-create 'checkbox
:format "%[%v%] Enable auto-save\n"
my-auto-save-enabled))
(widget-insert "\n")
;; Save button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(setq my-current-theme
(widget-value theme-widget))
(setq my-auto-save-enabled
(widget-value auto-save-widget))
(message "Settings saved"))
"Save Settings")
(use-local-map widget-keymap)
(widget-setup)))
```
### Dynamic Form
```elisp
(defun my-dynamic-list ()
(interactive)
(let (items-widget)
(switch-to-buffer "*Dynamic List*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Todo List\n\n")
(setq items-widget
(widget-create 'editable-list
:format "%v%i\n"
:entry-format "%i %d %v\n"
:value '("Buy groceries" "Write code")
'(editable-field :size 40)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((items (widget-value items-widget)))
(message "Todo items: %S" items)))
"Show Items")
(use-local-map widget-keymap)
(widget-setup)))
```
### Collecting Multiple Values
```elisp
(defun my-collect-values (widgets)
"Collect values from multiple widgets into alist."
(mapcar (lambda (pair)
(cons (car pair)
(widget-value (cdr pair))))
widgets))
(defun my-form-with-collection ()
(interactive)
(let (name-w email-w age-w widgets)
(switch-to-buffer "*Form*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Information\n\n")
(setq name-w (widget-create 'editable-field
:format "Name: %v\n"))
(setq email-w (widget-create 'editable-field
:format "Email: %v\n"))
(setq age-w (widget-create 'editable-field
:format "Age: %v\n"))
(setq widgets `((name . ,name-w)
(email . ,email-w)
(age . ,age-w)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((data (my-collect-values widgets)))
(message "Data: %S" data)))
"Submit")
(use-local-map widget-keymap)
(widget-setup)))
```
## Format Strings
Widget `:format` property controls display using escape sequences:
- `%v` - Widget value
- `%t` - Tag
- `%d` - Documentation string
- `%h` - Help-echo
- `%[` - Button prefix
- `%]` - Button suffix
- `%%` - Literal %
```elisp
;; Custom formats
(widget-create 'push-button
:format "Click %[here%] to continue\n"
:notify #'my-callback
"Continue")
(widget-create 'editable-field
:format "%t: %v (%h)\n"
:tag "Email"
:help-echo "[email protected]")
```
## Notifications and Callbacks
The `:notify` property specifies callback on widget change/activation.
**Callback signature:**
```elisp
(lambda (widget &optional changed-widget &rest event)
;; widget - the widget with :notify property
;; changed-widget - widget that actually changed (for composite widgets)
;; event - interaction event
...)
```
**Examples:**
```elisp
;; Simple notification
(widget-create 'checkbox
:notify (lambda (w &rest _)
(message "Checked: %s" (widget-value w)))
nil)
;; Access parent widget
(widget-create 'radio-button-choice
:notify (lambda (parent child &rest _)
(message "Parent value: %s" (widget-value parent))
(message "Child value: %s" (widget-value child)))
'(item "A")
'(item "B"))
;; Update other widgets
(let (field1 field2)
(setq field1
(widget-create 'editable-field
:notify (lambda (w &rest _)
(widget-value-set field2
(upcase (widget-value w)))
(widget-setup))))
(setq field2 (widget-create 'editable-field)))
```
## Keybindings
`widget-keymap` provides default bindings:
- `TAB` - `widget-forward` - Next widget
- `S-TAB` / `M-TAB` - `widget-backward` - Previous widget
- `RET` - `widget-button-press` - Activate button
- `C-k` - `widget-kill-line` - Kill to end of field
- `M-TAB` - `widget-complete` - Complete field (if supported)
**Custom keymap:**
```elisp
(defvar my-widget-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map widget-keymap)
(define-key map (kbd "C-c C-c") 'my-submit)
(define-key map (kbd "C-c C-k") 'my-cancel)
map))
(use-local-map my-widget-mode-map)
```
## Error Handling
### Validation
```elisp
(defun my-validate-email (widget)
(let ((value (widget-value widget)))
(unless (string-match-p "^[^@]+@[^@]+\\.[^@]+$" value)
(error "Invalid email format"))))
(widget-create 'editable-field
:format "Email: %v\n"
:notify (lambda (w &rest _)
(condition-case err
(my-validate-email w)
(error (message "Error: %s" (error-message-string err))))))
```
### Safe Value Retrieval
```elisp
(defun my-safe-widget-value (widget &optional default)
"Get widget value with fallback."
(condition-case nil
(widget-value widget)
(error default)))
```
## Buffer Cleanup
```elisp
(defun my-widget-cleanup ()
"Clean up widget buffer."
(interactive)
(when (eq major-mode 'my-widget-mode)
(remove-overlays)
(kill-buffer)))
;; With kill-buffer-hook
(defvar my-widget-mode-hook nil)
(add-hook 'my-widget-mode-hook
(lambda ()
(add-hook 'kill-buffer-hook
(lambda ()
(remove-overlays))
nil t)))
```
## Defining Custom Widgets
```elisp
(define-widget 'my-email-field 'editable-field
"Email input field with validation."
:format "Email: %v\n"
:size 40
:valid-regexp "^[^@]+@[^@]+\\.[^@]+$"
:notify (lambda (widget &rest ignore)
(let ((value (widget-value widget))
(regexp (widget-get widget :valid-regexp)))
(if (string-match-p regexp value)
(message "Valid email")
(message "Invalid email format")))))
;; Usage
(widget-create 'my-email-field)
```
**Inheritance:**
```elisp
(define-widget 'my-readonly-field 'editable-field
"Read-only text field."
:keymap (let ((map (copy-keymap widget-field-keymap)))
(suppress-keymap map)
map))
```
## Performance Considerations
**Minimize redraws:**
```elisp
;; Bad: Multiple setups
(widget-value-set widget1 val1)
(widget-setup)
(widget-value-set widget2 val2)
(widget-setup)
;; Good: Batch updates
(widget-value-set widget1 val1)
(widget-value-set widget2 val2)
(widget-setup)
```
**Use buffer-local variables:**
```elisp
(defvar-local my-widget-data nil
"Buffer-local widget storage.")
```
**Avoid redundant notifications:**
```elisp
(defvar my-updating nil)
(widget-create 'editable-field
:notify (lambda (w &rest _)
(unless my-updating
(setq my-updating t)
(my-expensive-update)
(setq my-updating nil))))
```
## Integration with Major Modes
```elisp
(define-derived-mode my-widget-mode special-mode "MyWidget"
"Major mode for widget-based interface."
:group 'my-widget
(setq truncate-lines t)
(setq buffer-read-only nil))
(defun my-widget-interface ()
(interactive)
(switch-to-buffer "*My Interface*")
(my-widget-mode)
(erase-buffer)
;; Build interface
(widget-insert "My Application\n\n")
;; ... create widgets ...
(use-local-map widget-keymap)
(widget-setup)
(goto-char (point-min))
(widget-forward 1))
```
## Debugging
```elisp
;; Inspect widget properties
(pp-eval-expression '(widget-get my-widget :value))
;; Check widget type
(widget-type my-widget)
;; View all properties
(let ((widget my-widget))
(while widget
(prin1 (car widget))
(terpri)
(setq widget (cdr widget))))
;; Trace notifications
(widget-create 'checkbox
:notify (lambda (&rest args)
(message "Notify called with: %S" args)
(backtrace))
t)
```
## Use Cases
### Configuration Editor
Customize package using widgets instead of Custom interface:
```elisp
(defun my-config-editor ()
(interactive)
(let (theme-w size-w)
(switch-to-buffer "*Config*")
(kill-all-local-variables)
(erase-buffer)
(widget-insert "Configuration\n\n")
(setq theme-w
(widget-create 'menu-choice
:tag "Theme"
:value my-theme
'(const light)
'(const dark)))
(setq size-w
(widget-create 'editable-field
:format "\nFont size: %v\n"
:value (number-to-string my-font-size)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest _)
(setq my-theme (widget-value theme-w))
(setq my-font-size
(string-to-number (widget-value size-w)))
(my-apply-config))
"Apply")
(use-local-map widget-keymap)
(widget-setup)))
```
### Search/Filter Interface
```elisp
(defun my-search-interface ()
(interactive)
(let (query-w type-w)
(switch-to-buffer "*Search*")
(kill-all-local-variables)
(erase-buffer)
(setq query-w
(widget-create 'editable-field
:format "Query: %v\n"
:size 50))
(setq type-w
(widget-create 'checklist
:format "\nTypes:\n%v\n"
'(const :tag "Functions" function)
'(const :tag "Variables" variable)
'(const :tag "Faces" face)))
(widget-create 'push-button
:notify (lambda (&rest _)
(my-perform-search
(widget-value query-w)
(widget-value type-w)))
"Search")
(use-local-map widget-keymap)
(widget-setup)))
```
### Wizard/Multi-step Form
```elisp
(defvar my-wizard-step 1)
(defvar my-wizard-data nil)
(defun my-wizard-next ()
(setq my-wizard-data
(plist-put my-wizard-data
(intern (format "step%d" my-wizard-step))
(my-collect-current-step)))
(setq my-wizard-step (1+ my-wizard-step))
(my-wizard-show))
(defun my-wizard-show ()
(erase-buffer)
(cond
((= my-wizard-step 1)
(my-wizard-step-1))
((= my-wizard-step 2)
(my-wizard-step-2))
(t
(my-wizard-finish)))
(widget-setup))
```
## Common Pitfalls
**Forgetting widget-setup:**
```elisp
;; Wrong
(widget-create 'editable-field)
;; User can't interact yet!
;; Correct
(widget-create 'editable-field)
(widget-setup)
```
**Not calling widget-setup after value-set:**
```elisp
;; Wrong
(widget-value-set widget "new value")
;; Widget not updated!
;; Correct
(widget-value-set widget "new value")
(widget-setup)
```
**Incorrect buffer setup:**
```elisp
;; Missing keymap
(erase-buffer)
(widget-create 'editable-field)
(widget-setup)
;; TAB won't navigate!
;; Correct
(erase-buffer)
(widget-create 'editable-field)
(use-local-map widget-keymap)
(widget-setup)
```
**Not cleaning overlays:**
```elisp
;; Memory leak
(erase-buffer)
(widget-create ...)
;; Proper cleanup
(erase-buffer)
(remove-overlays)
(widget-create ...)
```
## Resources
- Info manual: `C-h i m Widget RET`
- Source: `wid-edit.el`, `widget.el` in Emacs distribution
- Demo: `M-x widget-example` (if available)
- Tutorial: Official Emacs Widget Library manual