Complete reference for all vui.el functions, macros, and variables.
(vui-defcomponent NAME (PROPS...) [DOCSTRING] BODY...)Define a component named NAME.
- NAME
- Symbol naming the component
- PROPS
- List of prop names the component accepts
- DOCSTRING
- Optional documentation string
| Keyword | Required | Description |
|---|---|---|
:state | No | (VAR INITIAL)... - local state variables |
:on-mount | No | Form run after first render |
:on-update | No | Form run after re-render (not first) |
:on-unmount | No | Form run before removal |
:should-update | No | Return t to allow re-render, nil to skip |
:render | Yes | The render expression (must return a vnode) |
In all forms:
- Props as named variables (e.g.,
titlefor:titleprop) - State vars as named variables
childrenfor nested content
In :on-update and :should-update additionally:
props- Current props pliststate- Current state plistprev-props- Previous props plistprev-state- Previous state plist
(vui-defcomponent counter (initial-value)
"A counter component with increment button."
:state ((count initial-value))
:on-mount
(message "Counter mounted with %d" count)
:on-update
(unless (equal (plist-get prev-state :count)
(plist-get state :count))
(message "Count changed to %d" count))
:should-update
(not (equal (plist-get prev-state :count)
(plist-get state :count)))
:on-unmount
(message "Counter unmounted")
:render
(vui-fragment
(vui-text (format "Count: %d" count))
(vui-button "+" :on-click (lambda ()
(vui-set-state :count (1+ count))))))(vui-mount COMPONENT-VNODE &optional BUFFER-NAME) -> instanceMount a component as root and render to a buffer.
- COMPONENT-VNODE
- Created with
vui-component - BUFFER-NAME
- String (default
"*vui*") - Returns
- The root instance
(vui-mount (vui-component 'my-app :title "Hello") "*my-app*")(vui-rerender INSTANCE) -> instanceRe-render an existing instance, preserving component state.
Triggers a re-render of the component tree rooted at INSTANCE. Component state (including collapsed sections, internal state) is preserved through reconciliation. Memoized values are also preserved and will only recompute if their dependencies change.
Returns INSTANCE for chaining.
;; Re-render without changing anything (useful for forcing a redraw)
(vui-rerender instance)(vui-update INSTANCE NEW-PROPS) -> instanceUpdate instance props, invalidate all memos, and re-render.
Use this when external data has changed and cached computations should be discarded (e.g., after a file save, database update, or user-triggered refresh).
NEW-PROPS completely replaces the instance’s current props. All memoized values in the instance tree are invalidated, forcing recomputation on the next render. Component state is preserved through reconciliation.
Returns INSTANCE for chaining.
;; After save - data on disk changed, force recomputation
(vui-update instance (list :note (fetch-note)))(vui-update-props INSTANCE NEW-PROPS) -> instanceUpdate instance props and re-render, preserving memos.
Use this for periodic or speculative refreshes where data may not have changed. Memoized values are preserved and only recompute if their dependencies change, avoiding unnecessary expensive work.
NEW-PROPS completely replaces the instance’s current props. Component state is preserved through reconciliation.
Returns INSTANCE for chaining.
;; Idle timer - data probably hasn't changed, let memos decide
(vui-update-props instance (list :note (fetch-note)))| Function | Props | Memos | Use case |
|---|---|---|---|
vui-rerender | Keep | Preserved | Force redraw, same props |
vui-update-props | Replace | Preserved | Periodic refresh, data may not have changed |
vui-update | Replace | Invalidated | Data changed, force recomputation |
As a rule of thumb:
- Save hooks, manual refresh →
vui-update(data changed, recompute everything) - Idle timers, window focus →
vui-update-props(data probably unchanged, let memos skip work) - Internal triggers →
vui-rerender(just redraw with current props and state)
Interactive primitives (buttons, fields, checkboxes, selects) support TAB /
S-TAB keyboard navigation.
(vui-text CONTENT &rest PROPS) -> vnodeCreate a text node.
| Property | Type | Description |
|---|---|---|
:face | symbol | Emacs face for styling |
:key | any | Reconciliation key |
(vui-text "Hello" :face 'bold)
(vui-text (format "Count: %d" count))(vui-newline &optional KEY) -> vnodeCreate a newline.
(vui-space &optional WIDTH KEY) -> vnodeCreate WIDTH spaces (default 1).
(vui-fragment &rest CHILDREN) -> vnodeGroup multiple vnodes without adding structure.
(vui-fragment
(vui-text "Line 1")
(vui-newline)
(vui-text "Line 2"))(vui-button LABEL &rest PROPS) -> vnodeCreate a clickable button. By default, buttons render with brackets: LABEL becomes [LABEL].
| Property | Type | Description |
|---|---|---|
:on-click | function | Click handler |
:face | symbol | Button face |
:disabled | boolean | Disable button |
:max-width | integer | Max width including brackets (truncates) |
:no-decoration | boolean | Render without brackets |
:help-echo | string/nil | Tooltip (nil disables for performance) |
:tab-order | integer | Tab navigation order (-1 = skip) |
:keymap | keymap | Custom keymap active when on button |
:key | any | Reconciliation key |
(vui-button "Click me"
:on-click (lambda () (message "Clicked!"))
:face 'custom-button)
;; Renders: [Click me]
;; Truncated button
(vui-button "Very long label" :max-width 12)
;; Renders: [Very lo...]
;; Plain button (no brackets)
(vui-button "Click here" :no-decoration t)
;; Renders: Click here
;; Disable tooltip for performance (useful with many buttons)
(vui-button "Item" :help-echo nil)(vui-field &key VALUE SIZE PLACEHOLDER ON-CHANGE ON-SUBMIT KEY FACE SECRET) -> vnodeCreate a text input field. This is a simple string-based primitive. For typed
fields with parsing and validation, use vui-typed-field from vui-components.el.
| Property | Type | Description |
|---|---|---|
:value | string | Initial content (default “”) |
:size | integer | Width in characters |
:placeholder | string | Hint text (not yet rendered) |
:on-change | function | Called with value on change |
:on-submit | function | Called with value on RET |
:key | any | Reconciliation key / lookup key |
:face | symbol | Text face |
:secret | boolean | Hide input (for passwords) |
(vui-field :value name
:size 20
:on-change (lambda (v) (vui-set-state :name v)))
;; Password field
(vui-field :value password
:size 20
:secret t
:on-change (lambda (v) (vui-set-state :password v)))(vui-field-value KEY) -> string or nilGet current value of field with KEY, without triggering re-render.
(vui-field :key 'my-input :size 20)
(vui-button "Submit"
:on-click (lambda ()
(process (vui-field-value 'my-input))))(vui-checkbox &rest PROPS) -> vnodeCreate a checkbox.
| Property | Type | Description |
|---|---|---|
:checked | boolean | Whether checked |
:on-change | function | Called with boolean on toggle |
:label | string | Label after checkbox |
:key | any | Reconciliation key |
(vui-checkbox :checked enabled
:label "Enable feature"
:on-change (lambda (v) (vui-set-state :enabled v)))(vui-select &rest ARGS) -> vnodeCreate a selection button (minibuffer-based selection).
| Property | Type | Description |
|---|---|---|
:value | string | Current selection |
:options | list | List of options (strings or alist) |
:on-change | function | Called with selected value |
:prompt | string | Minibuffer prompt (default “Select: “) |
:key | any | Reconciliation key |
;; Simple list
(vui-select :value current-theme
:options '("light" "dark" "system")
:on-change (lambda (v) (vui-set-state :theme v)))
;; Alist with display values
(vui-select :value status
:options '(("active" . "Active")
("pending" . "Pending")
("closed" . "Closed"))
:on-change (lambda (v) (vui-set-state :status v)))(vui-hstack &rest ARGS) -> vnodeHorizontal layout. ARGS can start with options, then children.
| Option | Type | Default | Description |
|---|---|---|---|
:spacing | integer | 1 | Spaces between children |
:key | any | nil | Reconciliation key |
(vui-hstack :spacing 2
(vui-text "Label:")
(vui-field :size 10))(vui-vstack &rest ARGS) -> vnodeVertical layout. ARGS can start with options, then children.
| Option | Type | Default | Description |
|---|---|---|---|
:spacing | integer | 0 | Blank lines between children |
:indent | integer | 0 | Left indent in spaces |
:key | any | nil | Reconciliation key |
(vui-vstack :spacing 1 :indent 2
(vui-text "Item 1")
(vui-text "Item 2"))(vui-box CHILD &rest PROPS) -> vnodeFixed-width container for CHILD.
| Property | Type | Default | Description |
|---|---|---|---|
:width | integer | 20 | Width in characters |
:align | keyword | :left | :left, :center, :right |
:padding-left | integer | 0 | Left padding |
:padding-right | integer | 0 | Right padding |
:key | any | nil | Reconciliation key |
(vui-box (vui-text "Centered") :width 30 :align :center)(vui-table &rest ARGS) -> vnodeCreate a table layout. Cell contents can be strings or vnodes (including interactive widgets like buttons, fields, and components).
| Property | Type | Description |
|---|---|---|
:columns | list | List of column specs (see below) |
:rows | list | List of rows, each a list of cells |
:border | symbol | nil, :ascii, or :unicode |
:key | any | Reconciliation key |
When :border is set, cells are automatically padded with 1 space on each
side for readability (e.g., | value | instead of |value|).
| Property | Type | Description |
|---|---|---|
:header | string | Header text |
:width | integer | Target width for cell content |
:min-width | integer | Minimum width, expand as needed |
:grow | boolean | Pad short content, expand for long |
:truncate | boolean | Truncate long content with “…” |
:align | keyword | :left (default), :center, :right |
:width | :grow | :truncate | Content vs Width | Result |
|---|---|---|---|---|
| W | nil | nil | content < W | Column shrinks to content size |
| W | nil | nil | content > W | Overflow with broken bar (¦) |
| W | t | nil | content < W | Column = W, content padded |
| W | t | nil | content > W | Column expands to fit content |
| W | nil | t | content < W | Column shrinks to content size |
| W | nil | t | content > W | Column = W, content truncated with “…” |
| W | t | t | content < W | Column = W, content padded |
| W | t | t | content > W | Column = W, content truncated with “…” |
| nil | - | - | any | Column auto-sizes to content |
(vui-table
:columns '((:header "Name" :min-width 15)
(:header "Age" :width 5 :align :right))
:rows '(("Alice" "30")
("Bob" "25"))
:border :ascii)Cells can contain any vnode, including buttons and components:
(vui-table
:columns '((:header "Item" :width 20)
(:header "Action" :width 10))
:rows `(("Apple" ,(vui-button "[Buy]" :on-click (lambda () (message "Bought!"))))
("Banana" ,(vui-button "[Buy]" :on-click (lambda () (message "Bought!")))))
:border :ascii)For interactive tables, embed components with callbacks:
(vui-table
:columns '((:header "Name" :width 15)
(:header "Score" :width 8))
:rows (mapcar (lambda (item)
(list (plist-get item :name)
(vui-component 'score-input
:key (plist-get item :id)
:value (plist-get item :score)
:on-change on-score-change)))
items)
:border :ascii)Calculate column widths at render time based on content:
(let* ((max-len (apply #'max (mapcar #'length names)))
(col-width (max 18 (min 48 (+ max-len 2)))))
(vui-table
:columns `((:header "Name" :width ,col-width)
(:header "Value" :width 10))
:rows ...))See docs/examples/05-wine-tasting.el for a complete example demonstrating
interactive buttons, dynamic column widths, and real-time computed statistics.
(vui-list ITEMS RENDER-FN &optional KEY-FN &key VERTICAL INDENT SPACING) -> vnodeRender a list of items with proper reconciliation.
- ITEMS
- List of items to render
- RENDER-FN
(lambda (item) vnode)- renders each item- KEY-FN
(lambda (item) key)- extracts key (default: identity):vertical- If non-nil (default t), returns a
vstack; otherwise returns anhstack :indent- Left indentation in spaces (default 0)
:spacing- Blank lines between items for vertical (default 0), spaces for horizontal (default 1)
Returns a vstack for vertical lists, hstack for horizontal lists. This ensures
proper indent propagation when nested inside other layout containers.
;; Vertical list (default) - each item on its own line
(vui-list todos
(lambda (todo)
(vui-text (plist-get todo :text)))
(lambda (todo)
(plist-get todo :id)))
;; With indentation
(vui-list items #'vui-text nil :indent 2)
;; Horizontal list - items on same line with spacing
(vui-list tags
(lambda (tag) (vui-text (format "[%s]" tag)))
nil :vertical nil)(vui-component TYPE &rest PROPS-AND-CHILDREN) -> vnodeCreate a component vnode.
- TYPE
- Symbol naming a defined component
- PROPS-AND-CHILDREN
- Plist of props, optionally ending with
:children
(vui-component 'greeting :name "Alice")
(vui-component 'card :title "Hello" :children (list child1 child2))(vui-set-state KEY VALUE)Set state KEY to VALUE and schedule re-render.
Must be called from within a component context (event handler, lifecycle hook).
If VALUE is a function, it is called with the current value and the result is used as the new value. This is essential for async callbacks where captured variables may be stale.
;; Direct value
(vui-button "Increment"
:on-click (lambda ()
(vui-set-state :count (1+ count))))
;; Functional update (for async callbacks)
(run-with-timer 1 1
(vui-with-async-context
(vui-set-state :count #'1+))) ; Gets current value, returns incremented
;; Lambda for complex updates
(vui-set-state :items (lambda (old) (cons new-item old)))(vui-batch &rest BODY)Batch multiple state updates into a single re-render.
(vui-batch
(vui-set-state :name "Bob")
(vui-set-state :age 30)
(vui-set-state :active t))
;; Single re-render instead of three(vui-flush-sync)Force immediate re-render, bypassing any pending deferred timers.
(vui-with-async-context BODY...) -> functionCapture component context for use in async callbacks. Returns a function that, when called, restores the context and executes BODY.
Use this when you need vui-set-state in:
- Timer callbacks (
run-with-timer,run-with-idle-timer) - Emacs hook functions
- Process sentinels (when not using
vui-use-async) - Any callback that runs asynchronously
;; Timer example - use functional update for state-based values
(vui-use-effect ()
(let ((timer (run-with-timer 1 1
(vui-with-async-context
(vui-set-state :seconds #'1+)))))
(lambda () (cancel-timer timer))))
;; Hook example - (frame-width) is called fresh, no capture issue
(vui-use-effect ()
(let ((handler (vui-with-async-context
(vui-set-state :width (frame-width)))))
(add-hook 'window-size-change-functions handler)
(lambda ()
(remove-hook 'window-size-change-functions handler))))Note: You do NOT need this for widget callbacks (buttons, fields) or
vui-use-async loaders - those handle context automatically.
(vui-async-callback (ARGS...) BODY...) -> functionCreate an async callback that captures component context and accepts arguments.
Like vui-with-async-context, but the returned function accepts ARGS which are
bound when executing BODY.
Use this when an async operation passes data to your callback:
- API responses
- Process output
- Any async operation that returns a value
;; Fetch data and set state with the result
(vui-use-effect ()
(fetch-data-async
(vui-async-callback (result)
(vui-set-state :data result))))
;; Multiple arguments
(vui-use-effect ()
(my-api-call
(vui-async-callback (data status)
(vui-set-state :data data)
(vui-set-state :status status))))Compare with =vui-with-async-context=:
vui-with-async-context- fire-and-forget callbacks (timers, hooks)vui-async-callback- callbacks that receive data from async operations
(vui-use-effect (DEPS...) BODY...)Run side effect when DEPS change.
- Runs after first render
- Runs after re-render if any dep changed
- If BODY returns a function, it’s called as cleanup
;; Run once on mount
(vui-use-effect ()
(message "Mounted"))
;; Run when count changes
(vui-use-effect (count)
(message "Count: %d" count))
;; With cleanup
(vui-use-effect (user-id)
(let ((timer (run-with-timer 1 nil #'refresh)))
(lambda () (cancel-timer timer))))(vui-use-ref INITIAL-VALUE) -> (VALUE . nil)Create mutable ref that persists across renders.
Access via (car ref), set via (setcar ref new-value).
Modifying a ref does NOT trigger re-render.
(let ((timer-ref (vui-use-ref nil)))
(vui-use-effect ()
(setcar timer-ref (run-with-timer 1 1 #'tick))
(lambda () (cancel-timer (car timer-ref)))))(vui-use-callback (DEPS...) BODY)Create memoized callback that stays stable when DEPS unchanged.
(let ((handle-click (vui-use-callback (item-id)
(lambda () (delete-item item-id)))))
(vui-button "Delete" :on-click handle-click))(vui-use-callback* (DEPS...) :compare COMPARE BODY)Like vui-use-callback with configurable comparison.
COMPARE options:
eq- identity comparison (fast)equal- structural comparison (default)- function - custom
(lambda (old-deps new-deps) bool)
(vui-use-memo (DEPS...) BODY)Cache computed value, recompute only when DEPS change.
(let ((filtered (vui-use-memo (items filter)
(seq-filter (lambda (i)
(string-match-p filter (plist-get i :name)))
items))))
(vui-list filtered #'render-item))(vui-use-memo* (DEPS...) :compare COMPARE BODY)Like vui-use-memo with configurable comparison.
(vui-use-async KEY LOADER) -> plistAsynchronously load data using LOADER, identified by KEY.
- KEY
- Cache key; when it changes, a new load is triggered
- LOADER
(lambda (resolve reject) ...)- callresolvewith data orrejectwith error
Returns a plist:
| Key | Description |
|---|---|
:status | One of: pending, ready, or error |
:data | The loaded data (when ready) |
:error | Error message (when error) |
(let ((result (vui-use-async user-id
(lambda (resolve _reject)
(funcall resolve (fetch-user user-id))))))
(pcase (plist-get result :status)
('pending (vui-text "Loading..."))
('error (vui-text (format "Error: %s" (plist-get result :error))))
('ready (vui-text (format "User: %s" (plist-get result :data))))))(vui-defcontext NAME &optional DEFAULT-VALUE DOCSTRING)Define a context.
Creates:
NAME-context- The context objectNAME-provider- Function to provide valueuse-NAME- Hook to consume value
(vui-defcontext theme 'light "Current UI theme.")
;; Provide
(theme-provider 'dark
(vui-component 'my-app))
;; Consume (in component render)
(let ((theme (use-theme)))
(vui-text (format "Theme: %s" theme)))(vui-error-boundary &key FALLBACK ON-ERROR ID CHILDREN) -> vnodeCatch render errors in CHILDREN and display FALLBACK.
| Property | Type | Description |
|---|---|---|
:fallback | vnode | Displayed when error occurs |
:on-error | function | Called with error when caught |
:id | any | Unique identifier (auto-generated) |
:children | vnode | Protected content |
(vui-error-boundary
:id 'main-boundary
:fallback (vui-text "Something went wrong!" :face 'error)
:on-error (lambda (err) (log-error err))
:children (vui-component 'risky-component))(vui-reset-error-boundary ID)Reset error state for boundary ID, allowing retry.
| Variable | Default | Description |
|---|---|---|
vui-lifecycle-error-handler | warn | Handler for lifecycle errors |
vui-event-error-handler | warn | Handler for event errors |
vui-last-error | nil | Last error: (TYPE ERROR CONTEXT) |
Handler values:
warn- Display warning (default)message- Display in echo areasignal- Re-signal errorignore- Silently ignore- function - Custom
(lambda (hook-name err instance) ...)
| Variable | Default | Description |
|---|---|---|
vui-render-delay | 0.01 | Seconds before deferred render (nil=immediate) |
vui-timing-enabled | nil | Enable timing collection |
| Variable | Default | Description |
|---|---|---|
vui-debug-enabled | nil | Enable debug logging |
vui-debug-log-phases | (render mount update unmount state-change) | Phases to log |
(vui-inspect &optional INSTANCE)Display component tree in inspector buffer.
Shows hierarchical view with props and state for each component.
(vui-inspect-state &optional INSTANCE)Display only components with state.
(vui-get-instance-by-id ID &optional INSTANCE) -> instance or nilFind component instance by ID.
(vui-get-component-instances COMPONENT-TYPE &optional INSTANCE) -> listFind all instances of COMPONENT-TYPE.
(vui-report-timing &optional LAST-N)Display timing report in buffer.
Groups by component, shows time per phase.
(vui-get-timing) -> listReturn raw timing data.
Each entry: (:phase PHASE :component NAME :duration SECONDS :timestamp TIME)
(vui-clear-timing)Clear all timing data.
(vui-debug-show)Show the *vui-debug* buffer.
(vui-debug-clear)Clear debug log buffer.
(vui-defcomponent NAME (PROPS...)
:state ((VAR INIT) ...)
:on-mount FORM
:on-update FORM
:on-unmount FORM
:should-update FORM
:render VNODE)| Function | Description |
|---|---|
vui-text | Text content |
vui-newline | Line break |
vui-space | Horizontal space |
vui-fragment | Group without wrapper |
vui-button | Clickable button |
vui-field | Text input |
vui-checkbox | Toggle checkbox |
vui-select | Selection dropdown |
| Function | Description |
|---|---|
vui-hstack | Horizontal layout |
vui-vstack | Vertical layout |
vui-box | Fixed-width box |
vui-table | Table layout |
vui-list | List with keys |
| Hook | Description |
|---|---|
vui-use-effect | Side effects |
vui-use-ref | Mutable reference |
vui-use-callback | Memoized callback |
vui-use-memo | Memoized value |
vui-use-async | Async data loading |
| Function | Description |
|---|---|
vui-set-state | Update component state |
vui-batch | Batch multiple updates |
vui-flush-sync | Force immediate render |
| Macro/Function | Description |
|---|---|
vui-defcontext | Define context |
NAME-provider | Provide value |
use-NAME | Consume value |
| Function | Description |
|---|---|
vui-inspect | Show component tree |
vui-inspect-state | Show state only |
vui-report-timing | Performance report |
vui-debug-show | Show debug log |