Documentation
Views
The Ui module is Walnut’s view vocabulary: a small set of elements and attributes that map directly onto UIKit. It is always importable; most apps start with import Ui exposing (..).
Two ideas to hold on to:
- Everything returns data.
column [ spacing 8 ] [ text "hi" ]evaluates toVColumn [Spacing 8] [VText "hi"]— a value you can print, test, and diff. The renderer interprets it. - Every element names its UIKit counterpart. Walnut doesn’t hide the platform; it curates it.
Elements
| Function | Type | Rendered as |
|---|---|---|
text | String -> View msg | UILabel |
el | List (Attr msg) -> View msg -> View msg | container UIView (padding/centering/styling wrapper) |
column | List (Attr msg) -> List (View msg) -> View msg | vertical UIStackView |
row | List (Attr msg) -> List (View msg) -> View msg | horizontal UIStackView |
button | List (Attr msg) -> List (View msg) -> View msg | UIButton (configuration-based) |
textField | List (Attr msg) -> View msg | UITextField |
icon | List (Attr msg) -> String -> View msg | UIImageView + SF Symbol (e.g. icon [] "trash") |
image | List (Attr msg) -> String -> View msg | UIImageView from asset catalog name (scaleAspectFill) |
scroll | List (Attr msg) -> List (View msg) -> View msg | UIScrollView wrapping a column |
table | List (Attr msg) -> List (View msg) -> View msg | UITableView (sections as children) |
section | List (Attr msg) -> String -> List (View msg) -> View msg | table section + header title |
cell | List (Attr msg) -> List (View msg) -> View msg | table row; children are the cell content |
collection | List (Attr msg) -> List (View msg) -> View msg | UICollectionView (items as children) |
item | List (Attr msg) -> List (View msg) -> View msg | collection cell or map marker |
map | List (Attr msg) -> List (View msg) -> View msg | MKMapView; marker children are items |
sheet | List (Attr msg) -> msg -> View msg -> View msg | system sheet (UISheetPresentationController) |
navStack | List (Attr msg) -> msg -> List (View msg) -> View msg | UINavigationController (children = root…top) |
tabBar | List (Attr msg) -> (Int -> msg) -> List (View msg) -> View msg | UITabBarController (children = tab roots) |
spacer | View msg | greedy flexible space inside row/column |
native | String -> List (Attr msg) -> View msg | any UIKit class — see UIKit interop |
none | View msg | renders nothing (handy in conditionals) |
Notes:
texttakes no attributes. Style text by wrapping it:el [ fontSize 32, bold ] (text "Title"). Buttons style their own label:button [ fontSize 24 ] [ text "+" ].buttonchildren: atextand/or aniconbecome the button’s configuration title/image; anything richer is rendered inside the button.textFieldis controlled via attributes:content(current text),placeholder,onInput,secure. Fields keep focus across re-renders (they are reused by position, or bykeyif you provide one).table/section/cellmap onto a hostedUITableView. Putsectionnodes as table children; putcellnodes inside each section. Cell bodies are normal Walnut views (rows, text, icons). UseonTapon a cell for selection; give the table a stablekeyso scroll position survives re-renders.collection/itemmap onto a hostedUICollectionView(flow layout). Collection children must beitemnodes; item children are the cell content.onTap+keybehave like table cells.maphostsMKMapView. Set center withpropString/propFloatkeys"latitude","longitude","span"(degrees). Optional children areitemmarkers with the same lat/lon props, optionalcontenttitle, andonTap.sheet attrs onDismiss bodypresents a real system sheet. Swipe/grabber dismiss dispatchesonDismiss. Detents:propString "detent" "medium" | "large" | "mediumLarge"(default starts at medium). Bodies withheight fillorscrollstretch to the detent; intrinsic columns stay top-aligned. The sheet host insets for the keyboard. Keep custom dimmed cards asoverlay/dialog. Todo compose usessheet; Surfaces has a smoke demo. Siblingoverlay/dialognodes pin onto the topmost presented sheet (not under it). Multiplesheetnodes stack: later sheets present from the earlier sheet VC (Todo’s day/time/list picker sits on compose).navStack attrs onPop pageshosts aUINavigationController. Children are pages from root to top; give each page a stablekey. Interactive pop / edge swipe dispatchesonPop. Navigation bar is hidden — put your own back control in the page (or rely on the gesture). Textbook multi-tab apps: puttabBarat the root and give each tab its ownnavStack(Todo, Ledger). Do not wrap atabBarinside an outernavStack— that fights UIKit containment. Surfaces still demos a lonenavStackpush.tabBar attrs onSelect tabshosts aUITabBarController(system insets / Liquid Glass). Children are tab roots (index 0…). Title from each child’scontent; icon frompropString "systemImage" "house". Drive selection withpropInt "selected" i; taps callonSelect i. Prefer each child to be anavStackwith those attrs on the stack itself (wrappingnavStackinelused to leave an empty tab — the nav never embedded).native "ClassName"hosts any allowlistedUIViewsubclass by ObjC name (prop*/onNative*). Prefer this when you want to drop curated helpers (switch,button,textField, …). Details: uikit-interop.md.overlayremains for hand-rolled dimmed panels (pickers, custom cards). Prefersheetwhen you want system detents and swipe-to-dismiss. When asheetis up, overlays render on that sheet’s view.
Attributes
Layout
| Attribute | Meaning |
|---|---|
spacing n | gap between children of column/row |
padding n | inner margin on all sides |
paddingXY x y | horizontal / vertical inner margins |
width fill / width shrink / width (px n) | width behavior |
height fill / height shrink / height (px n) | height behavior |
centerX, centerY | center content on that axis |
alignLeft, alignRight, alignTop, alignBottom | cross-axis alignment for stack children |
safeArea | inset layout margins from the safe area guide |
scrollBottomInset n | extra bottom contentInset on scroll (clear floatBottom chrome) |
onRefresh msg | pull-to-refresh on scroll (UIRefreshControl); fires msg on pull |
Layout model, day 1: column/row are stack views; alignment attributes set the stack’s cross-axis alignment; spacer absorbs main-axis space; px sizes pin exact dimensions; fill lowers hugging so the element takes what’s left. The root view is edge-to-edge by default (so glass chrome can sample the home-indicator region); opt into safeArea on page roots when you want guide insets.
Walnut does not auto-theme for system Dark Mode. Explicit bg / fontColor values are fixed RGB. Host chrome that used adaptive UIKit colors (systemBackground, label, placeholderText) is kept clear / non-adaptive so a cream design system is not painted over black. Apps with a light-only palette should also set UIUserInterfaceStyle to Light (and/or window.overrideUserInterfaceStyle = .light).
Appearance
| Attribute | Meaning |
|---|---|
bg color | background color |
fontColor color | text color |
fontSize n | system font, size n |
fontWeight n | 100…900 (CSS-style); bold = 700, semibold = 600 |
textCenter | center-align label text |
rounded n | continuous-curve corner radius |
border w color | border width and color |
opacity x | 0.0 – 1.0 |
tint color | UIKit tint (buttons, icons) |
Liquid Glass
| Attribute | Meaning |
|---|---|
glass | glass material (iOS 26 UIGlassEffect / .glass() button; blur fallback earlier) |
glassProminent | prominent/interactive glass (.prominentGlass() button) |
Full story in Liquid Glass.
Events
| Attribute | Type | Fires |
|---|---|---|
onTap msg | msg -> Attr msg | button tap (touchUpInside) or tap gesture on any element |
onInput toMsg | (String -> msg) -> Attr msg | every text change in a textField |
onNative event msg | String -> msg -> Attr msg | a named UIControl event on a native element |
Text field state
| Attribute | Meaning |
|---|---|
content s | the field’s current text (keep it in your model) |
placeholder s | placeholder text |
secure | password entry |
key s | stable identity across renders (also useful for future keyed diffing) |
Colors
Color values come from:
rgb 1.0 0.5 0.0 -- components 0.0–1.0
rgba 0.0 0.0 0.0 0.5 -- with alpha
rgb255 255 149 0 -- 8-bit components
plus named system-ish colors: white black clear gray lightGray darkGray blue green red orange yellow purple pink teal indigo.
Putting it together
view : Model -> View Msg
view model =
scroll [ padding 20, spacing 14 ]
[ el [ fontSize 34, bold ] (text "Settings")
, column [ spacing 1, rounded 16, glass ]
[ settingRow "airplane" "Airplane Mode" model.airplane ToggleAirplane
, settingRow "wifi" "Wi-Fi" model.wifi ToggleWifi
]
, button [ onTap SignOut, glassProminent, tint red, centerX ]
[ text "Sign Out" ]
]
settingRow : String -> String -> Bool -> Msg -> View Msg
settingRow symbol label isOn msg =
row [ padding 14, spacing 12 ]
[ icon [ tint blue ] symbol
, text label
, spacer
, native "UISwitch"
[ propBool "on" isOn
, onNative "valueChanged" msg
]
]
Conditional rendering
Views are expressions, so use the language:
column [ spacing 8 ]
[ if model.hasError then
el [ fontColor red ] (text model.errorMessage)
else
none
, viewList model.items
]
Testing views
Because views are data, tests assert on structure without UIKit:
view { count = 3 }
--> VColumn [CenterX,CenterY,Spacing 16] [VText "3", VRow ...]
The Swift test suite does exactly this against whole programs (the toolchain), and the same property will power snapshot tooling later.