Layout primitives help you arrange UI elements in structured ways: horizontally, vertically, in boxes, or in tables.
Arranges children horizontally with optional spacing:
(vui-hstack
(vui-text "One")
(vui-text "Two")
(vui-text "Three"))Output:
One Two Three
Default spacing is 1 character. Customize with :spacing:
;; No spacing
(vui-hstack :spacing 0
(vui-text "A")
(vui-text "B")
(vui-text "C"))
;; Output: ABC
;; Extra spacing
(vui-hstack :spacing 3
(vui-text "One")
(vui-text "Two"))
;; Output: One Two(vui-hstack :spacing 2
(vui-button "Save" :on-click #'save-data)
(vui-button "Cancel" :on-click #'cancel)
(vui-button "Delete" :on-click #'delete-item
:face '(:foreground "red")))Output:
[Save] [Cancel] [Delete]
Arranges children vertically with optional spacing and indentation:
(vui-vstack
(vui-text "Line 1")
(vui-text "Line 2")
(vui-text "Line 3"))Output:
Line 1 Line 2 Line 3
Add blank lines between items with :spacing:
(vui-vstack :spacing 1
(vui-text "Paragraph 1")
(vui-text "Paragraph 2")
(vui-text "Paragraph 3"))Output:
Paragraph 1 Paragraph 2 Paragraph 3
Indent all children with :indent:
(vui-vstack :indent 4
(vui-text "- Item 1")
(vui-text "- Item 2")
(vui-text "- Item 3"))Output:
- Item 1 - Item 2 - Item 3
(vui-fragment
(vui-text "Tasks:")
(vui-newline)
(vui-vstack :spacing 1 :indent 2
(vui-text "• Write documentation")
(vui-text "• Add tests")
(vui-text "• Deploy")))Output:
Tasks: • Write documentation • Add tests • Deploy
Creates a fixed-width container with text alignment:
(vui-box (vui-text "Centered")
:width 20
:align :center)Output (20 chars wide):
Centered
| Value | Description |
|---|---|
:left | Align to left (default) |
:center | Center in box |
:right | Align to right |
(vui-vstack
(vui-box (vui-text "Left") :width 20 :align :left)
(vui-box (vui-text "Center") :width 20 :align :center)
(vui-box (vui-text "Right") :width 20 :align :right))Output:
Left
Center
Right
Add padding inside the box:
(vui-box (vui-text "Padded")
:width 20
:align :center
:padding-left 2
:padding-right 2)The padding reduces the available content width.
(vui-vstack
(vui-hstack
(vui-box (vui-text "Name:") :width 10 :align :right)
(vui-field :value name :size 20))
(vui-hstack
(vui-box (vui-text "Email:") :width 10 :align :right)
(vui-field :value email :size 20))
(vui-hstack
(vui-box (vui-text "Phone:") :width 10 :align :right)
(vui-field :value phone :size 20)))Output:
Name: [ ] Email: [ ] Phone: [ ]
Creates formatted tables with columns, rows, and optional borders.
(vui-table
:columns '((:min-width 8) (:min-width 10) (:min-width 8))
:rows '(("Alice" "Developer" "NYC")
("Bob" "Designer" "LA")
("Carol" "Manager" "Chicago")))Output:
Alice Developer NYC Bob Designer LA Carol Manager Chicago
(vui-table
:columns '((:header "Name" :width 8)
(:header "Role" :width 10)
(:header "Location" :width 10))
:rows '(("Alice" "Developer" "NYC")
("Bob" "Designer" "LA"))
:border :ascii)Output:
+----------+------------+------------+ | Name | Role | Location | +----------+------------+------------+ | Alice | Developer | NYC | | Bob | Designer | LA | +----------+------------+------------+
Bordered tables automatically add padding around cell content (| value | instead of |value|) for readability.
(vui-table
:columns '((:header "Name" :width 8)
(:header "Role" :width 10))
:rows '(("Alice" "Developer")
("Bob" "Designer"))
:border :unicode)Output:
┌──────────┬────────────┐ │ Name │ Role │ ├──────────┼────────────┤ │ Alice │ Developer │ │ Bob │ Designer │ └──────────┴────────────┘
(vui-table
:columns '((:header "ID" :width 5 :align :right)
(:header "Product" :width 12 :align :left)
(:header "Price" :width 8 :align :right))
:rows '(("1" "Widget" "$9.99")
("2" "Gadget" "$19.99")
("3" "Gizmo" "$29.99"))
:border :ascii)Output:
+-------+--------------+----------+ | ID | Product | Price | +-------+--------------+----------+ | 1 | Widget | $9.99 | | 2 | Gadget | $19.99 | | 3 | Gizmo | $29.99 | +-------+--------------+----------+
| Property | Description |
|---|---|
:header | Column header text |
:width | Target width for cell content |
:min-width | Minimum width, expands as needed |
:grow | If t, pad short content and expand for long |
:truncate | If t, truncate long content with “…” |
:align | :left, :center, or :right |
The :width, :grow, and :truncate options interact to control how columns handle content that doesn’t match the declared width:
: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 |
Notes:
:grow tacts as a minimum width guarantee:truncate tacts as a maximum width guarantee- Without
:grow, short content shrinks the column - Without
:truncate, long content either overflows (¦) or expands the column
Table cells can contain any vnode, including buttons, fields, and components:
(vui-table
:columns '((:header "Item" :width 12)
(:header "Qty" :width 6)
(:header "Action" :width 10))
:rows `(("Apples" "5" ,(vui-button "[Edit]" :on-click edit-apples))
("Oranges" "3" ,(vui-button "[Edit]" :on-click edit-oranges)))
:border :ascii)For interactive tables with complex state, embed components in cells:
(vui-table
:columns '((:header "Name" :width 15)
(:header "Score" :width 8))
:rows (mapcar (lambda (item)
(list (plist-get item :name)
(vui-component 'editable-score
:key (plist-get item :id)
:value (plist-get item :score)
:on-change handle-change)))
items)
:border :ascii)Calculate column widths dynamically based on content:
(let* ((max-name-len (apply #'max (mapcar #'length names)))
(col-width (max 18 (min 48 (+ max-name-len 2))))) ; min 18, max 48
(vui-table
:columns `((:header "Name" :width ,col-width)
(:header "Value" :width 10))
:rows ...))See docs/examples/05-wine-tasting.el for a complete example with:
- Interactive buttons in table cells
- Dynamic column width calculation
- Real-time statistics updates
Renders a list of items using a render function. Returns a vstack for vertical
lists (default) or hstack for horizontal lists, ensuring proper indent propagation.
(vui-list '("Apple" "Banana" "Cherry")
(lambda (item)
(vui-text (format "• %s" item))))Output:
• Apple • Banana • Cherry
For efficient updates, provide a key function:
(vui-list todos
(lambda (todo)
(vui-component 'todo-item :todo todo))
(lambda (todo)
(plist-get todo :id))) ; Key functionThe key function returns a unique identifier for each item.
Lists properly inherit indent from parent containers, or you can set it directly:
;; Inherits indent from parent vstack
(vui-vstack :indent 2
(vui-text "Items:")
(vui-list items #'vui-text))
;; Or set indent directly
(vui-list items #'vui-text nil :indent 2)(vui-defcomponent product-list ()
:state ((products '((:id 1 :name "Widget" :price 9.99)
(:id 2 :name "Gadget" :price 19.99))))
:render
(vui-vstack :spacing 1
(vui-list products
(lambda (product)
(vui-hstack
(vui-text (plist-get product :name))
(vui-text (format " - $%.2f"
(plist-get product :price)))))
(lambda (product)
(plist-get product :id)))))Layouts can be nested for complex structures:
(vui-vstack :spacing 1
;; Header row
(vui-hstack :spacing 2
(vui-text "Dashboard" :face 'bold)
(vui-button "Refresh"))
;; Two-column content
(vui-hstack :spacing 4
;; Left column
(vui-vstack
(vui-text "Statistics")
(vui-text "Users: 100")
(vui-text "Sales: 50"))
;; Right column
(vui-vstack
(vui-text "Actions")
(vui-button "Add User")
(vui-button "View Report"))))| Primitive | Purpose | Key Props |
|---|---|---|
vui-hstack | Horizontal arrangement | :spacing |
vui-vstack | Vertical arrangement | :spacing, :indent |
vui-box | Fixed-width container | :width, :align, :padding-left/right |
vui-table | Tabular data | :columns, :rows, :border |
vui-list | Dynamic list rendering | items, render-fn, key-fn, :indent, :spacing |
Exercise: Create a
dashboardcomponent that displays:
- A title “My Dashboard” at the top
- A table of tasks with columns: Status, Task, Due Date
- A button bar at the bottom with “Add Task” and “Clear Done” buttons
(vui-defcomponent dashboard ()
:state ((tasks '(("[ ]" "Write docs" "2024-01-15")
("[X]" "Add tests" "2024-01-10")
("[ ]" "Deploy" "2024-01-20"))))
:render
;; Your implementation here
)- Hooks - use-effect, use-ref, use-memo
- Primitives - Basic UI elements