LavaUI docs

LavaUI ↔ SwiftUI parity

LavaUI is deliberately SwiftUI-shaped (View, @ViewBuilder, @State, stacks, modifiers) but is not source-compatible. This document tracks what is still missing or only partially present, ordered so the next work is obvious.

It is an API gap list, not a bug list. Incorrect behaviour goes in issues.md. Product-specific needs live in their own gap docs. The broader framework assessment is lavaui-assessment.md.

Status legend

Tag Meaning
absent Not in the public API
partial Exists under a different shape, or only for some types
different Present by design, intentionally not SwiftUI-compatible

Guiding principles

  1. Match the call site when cheap. .padding(.horizontal, 8) is worth looking like SwiftUI; a full PreferenceKey system is not free.
  2. Keep Lava strengths. Yoga flex, @DrawState, dual overlays, Canvas gestures, Scene3D, client/compositor mode — do not delete these to chase source compatibility.
  3. One layer at a time. Prefer finishing a small cluster (edge padding + frame completeness) over scattering half-done modifiers.
  4. Document intentional divergence in api.md when we ship a different shape on purpose.

Tier 0 — Easy wins

Small surface area, Yoga/modifier machinery already exists, immediate payoff for every app. Start here.

0.1 Edge-aware padding — done (2026-08-08)

Status done
API Edge, EdgeInsets in EdgeInsets.swift; three .padding overloads
Tests Tests/LavaUITests/PaddingTests.swift
content.padding(8)                                              // all edges
content.padding(.horizontal, 12)
content.padding(.top, 4)
content.padding(EdgeInsets(top: 1, leading: 2, bottom: 3, trailing: 4))

ViewStyle / YogaBoxNode store EdgeInsets; Yoga gets per-edge YGNodeStyleSetPadding with LTR mapping (leading→left, trailing→right). Stack/button/overlay inits still take uniform Float and convert with .all(...). Padding after another modifier still forces an outer box.


0.2 View opacity

Status absent (only Color.opacity)
Today Transition alpha exists internally; no public .opacity

Proposed: .opacity(Float) -> … on View, stored on ViewStyle, applied at emit (multiply subtree alpha). Composes with transition alpha.

Cheap once emit has a place to multiply; no layout impact.


0.3 Offset (2D)

Status absent
Today Transition has temporary translation; no permanent .offset

Proposed: .offset(x:y:) — paint + hit-test shift without affecting Yoga layout (SwiftUI semantics). Layout-affecting position can wait.


0.4 Disabled / hidden as modifiers

Status partial — isEnabled: on Button/Toggle/Slider only
Proposed .disabled(Bool), .hidden() or .opacity(0) + hit-test skip

Tree-wide disable should block clicks on descendants. Hidden should remove from hit testing and ideally from layout (hidden that still occupies space vs a true collapse — pick SwiftUI’s “occupies space, not hit-testable, not drawn” unless we also want a collapse helper).


0.5 Lifecycle hooks

Status absent
Proposed .onAppear { }, .onDisappear { }, .onChange(of:initial:_:)

Appear/disappear fire from mount/unmount (and transition leave finish). onChange needs a retained previous value on the node or a small wrapper. High value for apps; moderate plumbing.


0.6 Fuller .frame

Status partial
Today width, height, minWidth, minHeight, alignment — no max, no ideal

Done: alignment:. Alignment is a HorizontalAlignment × VerticalAlignment pair with the nine named corners as statics, and OverlayAnchor is now a spelling of it rather than a second copy of the same switch. Stating one wraps the content in a StyleBoxNode laid out as a row — justifyContent is the horizontal half, alignItems the vertical — so the frame keeps its stated size and the content sits inside at its natural one.

The default is nil, not .center as in SwiftUI, because this modifier carries two meanings that SwiftUI splits. Without an alignment the size lands on the view's own node, which is what every existing caller means by it: a VStack given a width is that width and lays its children across it. With one, it is SwiftUI's frame — a box, with the content placed in it. nil records "never asked", which is not the same answer as .center, and is what keeps existing layouts still.

Worth knowing: it took a calendar to surface this. Text paints its glyphs at its box's leading edge, .frame made the text's box the whole 34pt cell, and the day number sat 4pt left of the highlight drawn around it. There was nowhere for the number to be centred in.

Still proposed — max/ideal sizes:

func frame(
    width: Dimension? = nil, height: Dimension? = nil,
    minWidth: Float? = nil, maxWidth: Float? = nil,
    minHeight: Float? = nil, maxHeight: Float? = nil,
    alignment: Alignment? = nil
) -> …

Yoga already has min/max width/height.


0.7 Edge padding on stacks (optional follow-on)

Once EdgeInsets exists, consider:

HStack(padding: EdgeInsets.horizontal(8), …) { … }

or keep stack padding uniform. Modifier-only is enough for Tier 0.


Tier 1 — Everyday app surface

Needed by most non-trivial UIs once Tier 0 is done.

Layout

Gap Status Notes
ZStack absent Alignment + layered children; apps fake it with overlays
Main-axis justification absent Documented; Spacer only. Yoga justify-content
GeometryReader absent Or a narrower onSizeChange if full reader is heavy
.border / stroke absent Expand fakes borders with nested fills
.background with a view partial Color only
.clipShape / .mask partial .clipped() = axis-aligned scissor
.shadow (2D) absent shadow3D only
.foregroundColor cascading partial Init args on Text/controls; no environment text color
.fixedSize / .aspectRatio / .layoutPriority absent

Controls & text

Gap Status Notes
Button label builder partial Button("Title") only
Text composition partial Single String; no Text+Text, attributes
Label, Link, SecureField absent
Picker partial ComboBox is the menu style: closed field + anchored dropdown, mouse-driven, no arrow keys
Stepper, ProgressView absent Build ad hoc when needed
List / Form / Section absent Recipes with stacks may suffice medium-term

Interaction

Gap Status Notes
Gesture system partial Canvas onGesture; stacks onClick/onHover; no DragGesture / .gesture
.onTapGesture / .onHover general partial Not on arbitrary views
Focus traversal + @FocusState partial Focus for fields exists; no Tab order, no binding
.keyboardShortcut, help/tooltip absent

Animation

Gap Status Notes
withAnimation / .animation(_:value:) absent Node-local Animated<T> only
Richer transitions partial opacity + slide only
matchedGeometryEffect absent

Environment & wrappers

Gap Status Notes
@Environment + custom keys partial Only theme + font via .theme/.font
@AppStorage different AppSettings exists; not a property wrapper
.id(_:) identity override absent

Presentation

Gap Status Notes
.sheet / alert / confirmation partial overlay(isPresented:) covers many cases
.contextMenu absent
.searchable absent

Tier 2 — Structure & scale

Gap Status Notes
NavigationStack / split / link absent Or a thin app-level router first
TabView absent
LazyHStack / LazyHGrid absent Vertical-only lazy today
Variable-height virtualization absent Fixed cell stride only (by design so far)
Both-axis ScrollView partial One axis per scroll view
ScrollViewReader / programmatic scroll absent
Group / AnyView / EquatableView absent
Custom Layout protocol absent Yoga is the only engine

Tier 3 — Platform & infrastructure

Tracked more fully in the assessment; listed here so parity work doesn’t forget them.

Gap Status Cost
Accessibility (AT-SPI, roles, labels) absent High; constrains the node model — don’t defer forever
IME / complex text absent TextField is Latin-oriented
HiDPI auto scale partial Manual FontStore.scale zoom
Text selection outside EditorView absent
Declarative App / Scene / WindowGroup different LavaApp.open + run is intentional for now
Multi-window polish partial WindowScope exists; not full Scene multi-window

Already present (do not re-implement)

Use as the baseline when reading the tables above.

Core: View, @ViewBuilder (no bare for), EmptyView, ForEach, retained identity, Observation.

State: @State, Binding, @Bindable, @DrawState (Lava-only).

Layout: HStack, VStack, Spacer, ScrollView, LazyVStack, LazyVGrid, Dimension (auto / pt / pct), flex grow/shrink.

Controls: Text, Button, TextField, Toggle, Slider, Divider, Expand, Image, Canvas, EditorView, MarkdownView.

Modifiers: padding (uniform), background color, hoverBackground, cornerRadius, frame (partial), flexGrow/Shrink, blur, backdropBlur, clipped, theme, font, transition, onDrop, overlay (composed + presented), agentId.

Other: menus, FileDialog, Scene3D, client/compositor frame path, agent server.


Intentional divergences (not gaps)

LavaUI Why
No buildArray / plain for in builders Index identity is unstable; force ForEach
Stack params include flexGrow, width, wraps, onClick Yoga + less modifier noise for common cases
ButtonStyle as a color/metrics struct Not a makeBody protocol — enough for themed controls
@DrawState Paint-only invalidation without body recompute
Dual overlay APIs Composed badge vs modal/presented popup
LavaApp.open / run Frame-driven Vulkan loop, client mode, menus
Fixed-height lazy cells Exact offset→index; no scrollbar jitter

Suggested implementation order

Concrete sequence for the next few PRs:

# Work Depends on
1 Edge + EdgeInsets + edge padding done
2 .opacity + .offset Emit alpha / hit-test offset
3 .frame max sizes Yoga min/max already
4 .disabled / .hidden Hit-test + emit flags
5 .onAppear / .onDisappear / .onChange Mount lifecycle
6 ZStack Alignment enum reuse
7 Stack main-axis justification Yoga justify-content
8 General .onTapGesture / .onHover Hit-test hooks
9 withAnimation bridge into Animated<T> Animation driver
10 Focus traversal FocusManager

After that, pick by product pressure (navigation, gestures, a11y, lazy horizontal).


When closing a gap

  1. Implement against the proposed API in this doc (or update the proposal first if reality disagrees).
  2. Add a short example to api.md.
  3. Mark the row here done with a one-line note (and date if useful).
  4. Prefer a unit or layout test where the behaviour is structural (padding edges, frame max, ZStack order).

Changelog

Date Note
2026-08-08 Initial inventory from LavaUI sources + api.md + assessment
2026-08-08 Tier 0.1 edge-aware padding shipped (Edge / EdgeInsets / overloads)
2026-08-24 Tier 0.6 frame alignment shipped (Alignment / OverlayAnchor)

docs/swiftui-parity.md