Vui.el
Declarative, component-based UI library for Emacs. React-like components with state, hooks, reconciliation, and layouts - rendered using native Emacs widgets.
Install / Use
/learn @d12frosted/Vui.elREADME
#+TITLE: vui.el #+AUTHOR: vui.el
#+begin_html
<p align="center"> <a href="https://melpa.org/#/vui"><img alt="MELPA" src="https://melpa.org/packages/vui-badge.svg"/></a> <img alt="version" src="https://img.shields.io/github/v/tag/d12frosted/vui.el?label=version"/><a href="https://github.com/sponsors/d12frosted"><img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-d12frosted-pink?logo=githubsponsors&logoColor=white"/></a>
</p> #+end_htmlDeclarative, 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.
#+begin_quote The API is stable and used in [[#built-with-vui][real-world projects]]. #+end_quote
- 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
#+begin_src elisp ;;; -- lexical-binding: t -- (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") #+end_src
Result: A buffer with text "Count: 0" and a clickable button. Each click updates the count and re-renders.
- More Examples
** Props and Composition
#+begin_src elisp (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"))) #+end_src
** Form Input
#+begin_src elisp (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))))) #+end_src
** Lifecycle Hooks
#+begin_src elisp (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))) #+end_src
** Context for Theme
#+begin_src elisp (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"))) #+end_src
- Installation
** MELPA
#+begin_src elisp (use-package vui :ensure t) #+end_src
** Manual
Clone this repository and add to your load-path:
#+begin_src elisp (add-to-list 'load-path "/path/to/vui.el") (require 'vui) #+end_src
- Documentation
| Document | Description | |-----------------+----------------------------------| | [[file:docs/guide/01-getting-started.org][Getting Started]] | Installation and first component | | [[file:docs/guide/02-components.org][Components]] | Props, state, composition | | [[file:docs/guide/03-primitives.org][Primitives]] | Text, button, field, etc. | | [[file:docs/guide/04-layout.org][Layout]] | hstack, vstack, table, list | | [[file:docs/guide/05-hooks.org][Hooks]] | vui-use-effect, vui-use-ref, vui-use-memo | | [[file:docs/guide/06-context.org][Context]] | Sharing data across components | | [[file:docs/guide/07-lifecycle.org][Lifecycle]] | on-mount, on-update, on-unmount | | [[file:docs/guide/08-error-handling.org][Error Handling]] | Error boundaries | | [[file:docs/guide/09-performance.org][Performance]] | Optimization techniques | | [[file:docs/guide/10-dev-tools.org][Developer Tools]] | Inspector, profiler, debugging | | [[file:docs/reference/api.org][API Reference]] | Complete function reference |
- Deep Dives
In-depth tutorials walking through real-world usage:
- [[https://www.d12frosted.io/posts/2025-12-01-vui-quickstart][Quickstart]] — 15-minute introduction to props, state, and composition
- [[https://www.d12frosted.io/posts/2025-12-02-vui-real-ui][Building a File Browser]] — Practical walkthrough of component decomposition
- [[https://www.d12frosted.io/posts/2025-12-03-vui-context-and-composition][Context and Composition]] — Prop drilling solutions and composition patterns
- [[https://www.d12frosted.io/posts/2025-12-04-vui-hooks-deep-dive][Lifecycle Hooks]] — on-mount, on-unmount, use-effect, use-async and cleanup patterns
- [[https://www.d12frosted.io/posts/2025-12-05-vui-optimisation-hooks][Optimisation Hooks]] — use-ref, use-callback, use-memo and when to use them
- [[https://www.d12frosted.io/posts/2025-12-06-vui-under-the-hood][Under the Hood]] — Virtual nodes, instances, reconciliation, and the render cycle
- [[https://www.d12frosted.io/posts/2025-12-07-vui-patterns-and-pitfalls][Patterns and Pitfalls]] — Practical patterns and common mistakes
For those curious about implementation details:
- [[https://www.d12frosted.io/posts/2025-12-14-implicit-identity-call-order][Implicit Identity]] — How hooks use call order as implicit identity
- [[https://www.d12frosted.io/posts/2025-12-16-cursor-preservation-buffer-rewrite][Cursor Preservation]] — Preserving cursor position across buffer rewrites
- Examples
See [[file:docs/examples/][docs/examples/]] for complete, runnable examples:
- [[file:docs/examples/01-hello-world.el][Hello World]] — Basic examples from the getting started guide
- [[file:docs/examples/02-todo-app.el][Todo App]] — Full todo application with add/remove/filter
- [[file:docs/examples/03-forms.el][Forms]] — Form validation, multi-step wizards, settings
- [[file:docs/examples/04-file-browser.el][File Browser]] — Directory navigation with sorting and search
- [[file:docs/examples/05-wine-tasting.el][Wine Tasting]] — Dynamic tables with interactive cells, computed statistics
- [[file:docs/examples/06-collapsible.el][Collapsible]] — Expandable/collapsible sections, FAQ style, nested sections
- [[file:docs/examples/07-semantic-text.el][Semantic Text]] — Headings, emphasis, status messages with customizable faces
- [[file:docs/examples/08-typed-fields.el][Typed Fields]] — Integer/float/symbol input with validation, shopping cart, forms
- Available Components
** Primitives
| Component | Description | |----------------+--------------------------------| | =vui-text= | Styled text | | =vui-newline= | Line break | | =vui-space= | Horizontal spacing | | =vui-button= | Clickable button with callback | | =vui-field= | Text input field | | =vui-checkbox= | Toggle checkbox | | =vui-select= | Selection from options | | =vui-fragment= | Group elements without wrapper |
** Layout
| Component | Description | |--------------+----------------------------------------| | =vui-hstack= | Horizontal layout with spacing | | =vui-vstack= | Vertical layout with spacing/indent | | =vui-box= | Fixed-width container with alignment | | =vui-table= | Table with headers, borders, alignment | | =vui-list= | Dynamic list with key-based reconcile |
** Higher-Level Components (vui-components.el)
| Component | Description | |--------------------+--------------------------------------------------| | =vui-collapsible= | Expandable/collapsible section with header | | =vui-typed-field= | Input with type conversion and validation | | =vui-integer-field=, =vui-float-field=, etc. | Shortcuts for common types |
#+begin_src elisp (require 'vui-components)
(vui-collapsible :title "FAQ" (vui-text "Hidden by default, click to reveal."))
(vui-collapsible :title "Details" :initially-expanded t (vui-text "Visible on load."))
;; Typed field with validation (vui-integer-field :value 42 :min 0 :max 100 :show-error 'inline :on-change (lambda (n) (vui-set-state :count n))) #+end_src
** Semantic Text Components (vui-components.el)
Thin wrappers around =vui-text= with customizable faces:
| Component | Inherits From | |---------------------------------+-----------------------------| | =vui-heading= / =vui-heading-N= | =outline-1= ... =outline-8= | | =vui-strong= | =bold= | | =vui-italic= | =italic= | | =vui-muted= | =shadow= | | =vui-code= | =fixed-pitch= | | =vui-error= | =error= | | =vui-warning= | =warning= | | =vui-success= | =success= |
#+begin_src elisp (require 'vui-components)
(vui-vstack (vui-heading-1 "Main Title") (vui-heading-2 "Subsection") (vui-strong "Important!") (vui-muted "Less important...") (vui-code "inline-code") (vui-error "Something went wrong"))
;; Or with :level for programmatic use (vui-heading "Dyn
