Back to skills
SkillHub ClubShip Full StackFull StackFrontend

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.

Stars
3
Hot score
80
Updated
March 20, 2026
Overall rating
C2.0
Composite score
2.0
Best-practice grade
C61.1

Install command

npx @skill-hub/cli install hugoduncan-library-skills-widget
emacswidgetuiformselisp

Repository

hugoduncan/library-skills

Skill path: plugins/emacs-libraries/skills/widget

A guide to using the Emacs Widget Library for creating interactive UI elements in buffers.

Open repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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
widget | SkillHub