vui.el is a declarative, component-based UI library for Emacs. If you’ve used React, Vue, or similar frameworks, you’ll feel right at home. If not, don’t worry - the concepts are simple:
- Declarative: Describe what your UI should look like, not how to update it
- Component-based: Build UIs from small, reusable pieces
- Reactive: When state changes, the UI updates automatically
Think of vui.el as “React for Emacs buffers” - but designed specifically for Emacs’s text-based, keyboard-driven environment.
Clone the repository and add it to your load path:
(add-to-list 'load-path "/path/to/vui.el")
(require 'vui)vui.el has no external dependencies. It only requires built-in Emacs libraries:
cl-lib(structures and utilities)wid-edit(widget.el for input handling)
Let’s start with the simplest possible component:
(vui-defcomponent hello-world ()
:render (vui-text "Hello, World!"))This defines a component named hello-world that renders the text “Hello, World!”.
To see it in action:
(vui-mount (vui-component 'hello-world) "*my-first-vui*")A new buffer named *my-first-vui* will appear with your greeting.
Let’s break down what happened:
vui-defcomponentdefines a new component type (like a template)hello-worldis the component’s name()means this component takes no props (inputs):renderspecifies what the component displaysvui-textcreates a text nodevui-mountcreates a live instance and renders it to a buffer
Static text is boring. Let’s make a button:
(vui-defcomponent click-counter ()
:state ((count 0))
:render
(vui-fragment
(vui-text (format "Clicked: %d times" count))
(vui-newline)
(vui-button "Click me!"
:on-click (lambda ()
(vui-set-state :count (1+ count))))))Mount it:
(vui-mount (vui-component 'click-counter) "*counter*")Now you have a button! Press RET on it to increment the counter.
:state ((count 0))- Defines local state withcountstarting at 0vui-fragment- Groups multiple elements without adding wrapper textvui-newline- Inserts a line breakvui-button- Creates a clickable button:on-click- Callback when button is activatedvui-set-state- Updates state and triggers re-render
The magic: when you call vui-set-state, vui.el automatically re-renders the component with the new state. You never manually update the buffer.
Components can receive data from their parent via props:
(vui-defcomponent greeter (name)
:render (vui-text (format "Hello, %s!" name)))
;; Use it:
(vui-mount (vui-component 'greeter :name "Alice") "*greeter*")
;; Shows: "Hello, Alice!"Props are listed in the argument list (name) and accessed as regular variables in the render function.
The real power comes from combining components:
(vui-defcomponent greeting-card (title)
:state ((expanded nil))
:render
(vui-fragment
(vui-button (if expanded "▼" "▶")
:on-click (lambda ()
(vui-set-state :expanded (not expanded))))
(vui-text (format " %s" title))
(vui-newline)
(when expanded
(vui-fragment
(vui-text " Welcome to vui.el!")
(vui-newline)
(vui-text " This is a collapsible card.")))))
(vui-mount (vui-component 'greeting-card :title "My Card") "*card*")Click the arrow to expand/collapse the card.
Notice how the render function is just Emacs Lisp. You can use when, if, cl-loop, or any other control flow. The render function runs every time state changes, and vui.el figures out what to update in the buffer.
vui.el supports text input via vui-field:
(vui-defcomponent name-form ()
:state ((name ""))
:render
(vui-fragment
(vui-text "Enter your name: ")
(vui-field :value name
:size 20
:on-change (lambda (new-value)
(vui-set-state :name new-value)))
(vui-newline)
(vui-newline)
(vui-text (if (string-empty-p name)
"Type something above..."
(format "Hello, %s!" name)))))
(vui-mount (vui-component 'name-form) "*name-form*")The greeting updates as you type!
| Function | Purpose |
|---|---|
vui-defcomponent | Define a new component type |
vui-component | Create a component vnode |
vui-mount | Render component to a buffer |
vui-set-state | Update component state |
vui-text | Render text |
vui-button | Render clickable button |
vui-field | Render text input field |
vui-fragment | Group elements without wrapper |
vui-newline | Insert line break |
Here’s a challenge to test your understanding:
Exercise: Create a component called
todo-itemthat:
- Takes a
textprop- Has a
donestate (initiallynil)- Shows a checkbox (
[ ]or[X]) as a button that togglesdone- Shows the text, with
shadowface when done
Hint: Use :face property on vui-text to style text.
;; Your solution here:
(vui-defcomponent todo-item (text)
:state ((done nil))
:render
;; ... fill in the rest
)See examples/01-hello-world.el for the solution.
Now that you understand the basics, explore:
- Components - Deep dive into defcomponent, props, and state
- Primitives - All the built-in UI elements
- Layout - Arrange elements with hstack, vstack, tables
- Hooks - Side effects, refs, and memoization
Or jump straight to a complete todo app example.