Skip to content

Declarative, component-based UI library for Emacs. React-like components with state, hooks, reconciliation, and layouts - rendered using native Emacs widgets.

License

Notifications You must be signed in to change notification settings

d12frosted/vui.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vui.el

MELPA version

Declarative, component-based UI framework for Emacs

Build reactive UIs in Emacs using familiar patterns from React and other modern UI frameworks. Define components with local state, props, lifecycle hooks, and automatic re-rendering.

The API is stable and used in real-world projects.

Features

  • Components — Reusable UI building blocks with props and local state
  • Reactive State — Automatic re-rendering when state changes
  • Hooks — vui-use-effect, vui-use-ref, vui-use-memo, vui-use-callback
  • Context — Share data across component trees without prop drilling
  • Layout Primitives — hstack, vstack, box, table, list
  • Error Boundaries — Graceful error handling with fallback UI
  • Developer Tools — Component inspector, timing profiler, debug logging

Quick Example

(require 'vui)

;; Define a component
(vui-defcomponent counter ()
  :state ((count 0))
  :render
  (vui-fragment
   (vui-text (format "Count: %d" count))
   (vui-newline)
   (vui-button "Increment"
               :on-click (lambda ()
                           (vui-set-state :count (1+ count))))))

;; Mount it
(vui-mount (vui-component 'counter) "*counter*")

Result: A buffer with text “Count: 0” and a clickable button. Each click updates the count and re-renders.

More Examples

Props and Composition

(vui-defcomponent greeting (name)
  :render
  (vui-text (format "Hello, %s!" name)))

(vui-defcomponent app ()
  :render
  (vui-vstack
   (vui-component 'greeting :name "Alice")
   (vui-component 'greeting :name "Bob")))

Form Input

(vui-defcomponent name-form ()
  :state ((name ""))
  :render
  (vui-fragment
   (vui-text "Enter name: ")
   (vui-field :value name
              :size 20
              :on-change (lambda (v) (vui-set-state :name v)))
   (vui-newline)
   (vui-text (if (string-empty-p name)
                 "Type something..."
               (format "Hello, %s!" name)))))

Lifecycle Hooks

(vui-defcomponent timer ()
  :state ((seconds 0))
  :on-mount
  (let ((timer (run-with-timer 1 1
                 (vui-with-async-context
                   (vui-set-state :seconds #'1+)))))
    (lambda () (cancel-timer timer)))

  :render
  (vui-text (format "Elapsed: %d seconds" seconds)))

Context for Theme

(vui-defcontext theme 'light)

(vui-defcomponent themed-button (label)
  :render
  (let ((theme (vui-use-theme)))
    (vui-button label
                :face (if (eq theme 'dark)
                          'custom-button-pressed
                        'custom-button))))

(vui-defcomponent app ()
  :render
  (theme-provider 'dark
    (vui-component 'themed-button :label "Click me")))

Installation

MELPA

(use-package vui
  :ensure t)

Manual

Clone this repository and add to your load-path:

(add-to-list 'load-path "/path/to/vui.el")
(require 'vui)

Documentation

DocumentDescription
Getting StartedInstallation and first component
ComponentsProps, state, composition
PrimitivesText, button, field, etc.
Layouthstack, vstack, table, list
Hooksvui-use-effect, vui-use-ref, vui-use-memo
ContextSharing data across components
Lifecycleon-mount, on-update, on-unmount
Error HandlingError boundaries
PerformanceOptimization techniques
Developer ToolsInspector, profiler, debugging
API ReferenceComplete function reference

Deep Dives

In-depth tutorials walking through real-world usage:

For those curious about implementation details:

Examples

See docs/examples/ for complete, runnable examples:

  • Hello World — Basic examples from the getting started guide
  • Todo App — Full todo application with add/remove/filter
  • Forms — Form validation, multi-step wizards, settings
  • File Browser — Directory navigation with sorting and search
  • Wine Tasting — Dynamic tables with interactive cells, computed statistics

Available Components

Primitives

ComponentDescription
vui-textStyled text
vui-newlineLine break
vui-spaceHorizontal spacing
vui-buttonClickable button with callback
vui-fieldText input field
vui-checkboxToggle checkbox
vui-selectSelection from options
vui-fragmentGroup elements without wrapper

Layout

ComponentDescription
vui-hstackHorizontal layout with spacing
vui-vstackVertical layout with spacing/indent
vui-boxFixed-width container with alignment
vui-tableTable with headers, borders, alignment
vui-listDynamic list with key-based reconcile

Hooks

HookDescription
vui-use-effectSide effects with cleanup
vui-use-refMutable reference (no re-render on change)
vui-use-callbackStable callback reference
vui-use-memoCached computed value
vui-use-asyncAsync data loading with cache

Using Shorter Names (Shorthands)

If you prefer the cleaner React-style names without the vui- prefix, you have two options:

Emacs 28+: Read Symbol Shorthands

Add to your file’s local variables:

;; Local Variables:
;; read-symbol-shorthands: (("defc" . "vui-defc") ("use-" . "vui-use-"))
;; End:

This lets you write defcomponent instead of vui-defcomponent and use-effect instead of vui-use-effect.

Aliases

Define aliases in your init file:

(defalias 'defcomponent 'vui-defcomponent)
(defalias 'defcontext 'vui-defcontext)
(defalias 'use-effect 'vui-use-effect)
(defalias 'use-ref 'vui-use-ref)
(defalias 'use-callback 'vui-use-callback)
(defalias 'use-memo 'vui-use-memo)
(defalias 'use-async 'vui-use-async)

Developer Tools

;; Inspect component tree
(vui-inspect)

;; View state of all components
(vui-inspect-state)

;; Profile render performance
(setq vui-timing-enabled t)
;; ... interact with your app ...
(vui-report-timing)

;; Debug render cycles
(setq vui-debug-enabled t)
(vui-debug-show)

Requirements

  • Emacs 27.1 or later
  • Built-in widget.el (included with Emacs)

Known Limitations

Emacs 29: Single-widget TAB navigation

On Emacs 29.x, pressing TAB in a buffer with only one tabbable widget (e.g., a single field or button) will error with “No buttons or fields found”. This is a bug in Emacs’s widget-move fixed in Emacs 30.

Workaround: Add a second widget, or use mouse/direct interaction. Buffers with multiple widgets work fine.

Major Mode

VUI buffers use vui-mode, a major mode derived from special-mode. This provides:

  • TAB / S-TAB — Navigate between widgets (buttons, fields)
  • RET — Activate widget at point
  • Standard special-mode bindings (q to quit, g to revert, etc.)

Extending with Custom Keybindings

Users can add bindings to vui-mode-map. For example, to enable ace-link-vui for quick widget navigation:

(define-key vui-mode-map (kbd "o") #'ace-link-vui)

Deriving Custom Modes

Packages can derive their own modes from vui-mode to add custom keybindings:

(define-derived-mode my-sidebar-mode vui-mode "MySidebar"
  "Custom mode for my sidebar."
  ;; Custom keybindings
  (define-key my-sidebar-mode-map (kbd "q") #'my-sidebar-close)
  (define-key my-sidebar-mode-map (kbd "g") #'my-sidebar-refresh))

When using a derived mode, enable it before calling vui-mount or vui-render. VUI will detect the derived mode and preserve it across re-renders.

Architecture

vui.el implements a React-like architecture:

  1. Virtual DOM — Components return vnodes (virtual nodes)
  2. Reconciliation — Diffing algorithm to minimize DOM updates
  3. Component Instances — Maintain state and lifecycle across renders
  4. Hooks System — Composable state and effects
  5. Context Stack — Provider/consumer pattern for shared state

Contributing

Contributions welcome! Please:

  1. Check existing issues before opening new ones
  2. Include tests for new features
  3. Follow existing code style
  4. Update documentation as needed

Related Projects

  • ace-link-vui — Ace-link style navigation for VUI buffers

Built with VUI

  • vulpea-ui — Sidebar UI for vulpea notes
  • vulpea-journal — Journaling system with calendar widgets
  • brb — Barberry Garden management system

License

GPL-3.0

Acknowledgments

Inspired by:

  • React (component model, hooks)
  • Svelte (reactivity)
  • SolidJS (fine-grained updates)
  • Emacs widget.el (underlying implementation)

About

Declarative, component-based UI library for Emacs. React-like components with state, hooks, reconciliation, and layouts - rendered using native Emacs widgets.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published