Skip to content

UI elements

Build a tree by calling ui constructors with a Luau table. ui.row{ ... } is shorthand for ui.row({ ... }); named entries are properties and numeric entries are ordered children. Return the tree from render(ctx), popout(ctx), or a component view. Constructors return Element values; do not manufacture kind yourself.

Start with your first widget or a menu. All constructors share layout, styling, and interaction properties. Components and shaders have their own pages.

Constructor Layout
ui.row Children from left to right; gap separates them.
ui.column Children from top to bottom. Use a fixed h and scroll = true for a scrollable list.
ui.stack Overlapping children; align positions each child. Useful for a material, shader, or path behind content.
ui.box A basic container for a background, hit target, or one child. An empty w = 0, h = 0 box draws nothing.
ui.row{ gap = 8, pad = 10, radius = 12, fill = "surface",
ui.symbol{ name = "clock", size = 16, color = "accent" },
ui.text{ text = ctx.data.clock, weight = "semibold" },
}

ui.row

row: (props: Props) -> Element,

ui.column

column: (props: Props) -> Element,

ui.stack

stack: (props: Props) -> Element,

ui.box

box: (props: Props) -> Element,

Element

export type Element = { kind: number }

Fields: kind

Constructor / property Behavior
ui.text.text Displayed string. style selects body, bold, mono, small, caption, or rounded presentation.
font system, sans (Google Sans Flex), rubik, or symbols; custom fonts are bundled. With symbols, text is a Material Symbols icon name and filled = true selects its filled style.
size, weight, color Explicit font size in points, font weight, and color.
maxWidth, lines Bound text width and line count; without explicit limits text uses its measured size.
reserve Measure a stable placeholder string instead of the displayed text, for example reserve = "00:00:00" for a clock.
ui.symbol.name An SF Symbol name, with optional size, weight, and color.
ui.icon.app, window, pid An app bundle ID, opaque managed-window ID, or process ID. Use one identity source per icon; placeholder supplies a fallback symbol and color a tint.
ui.text{ font = "symbols", text = "dashboard", filled = true, size = 22 }
ui.icon{ app = "com.apple.finder", placeholder = "app", w = 20, h = 20 }

ui.text

text: (props: TextProps) -> Element,

TextProps

export type TextProps = Props & {
text: string,
style: TextStyle?,
font: Font?,
filled: boolean?,
size: number?,
weight: Weight?,
maxWidth: number?,
lines: number?,
color: Color?,
reserve: string?,
}

Fields: text, style, font, filled, size, weight, maxWidth, lines, color, reserve

TextStyle

export type TextStyle = "body" | "bold" | "mono" | "small" | "caption" | "rounded"

Font

export type Font = "system" | "sans" | "rubik" | "symbols"

Weight

export type Weight = "ultraLight" | "thin" | "light" | "regular" | "medium" | "semibold" | "bold" | "heavy" | "black"

ui.symbol

symbol: (props: SymbolProps) -> Element,

SymbolProps

export type SymbolProps = Props & {
name: string,
size: number?,
weight: Weight?,
color: Color?,
}

Fields: name, size, weight, color

ui.icon

icon: (props: IconProps) -> Element,

IconProps

export type IconProps = Props & {
app: string?,
window: string?,
pid: number?,
placeholder: string?,
color: Color?,
}

Fields: app, window, pid, placeholder, color

ui.image.image accepts a contained image path, a qualified asset, or a host-issued Resource. A relative file such as assets/cat.gif resolves through the widget, shell, selected profile, and bundled default folders. Setting radius clips the image to rounded corners.

Animated GIF, PNG, WebP, and HEIC images play automatically. playing = false holds the current frame; speed scales playback speed. For Resource images, thumbnail requests 1–2048 pixels and defaults to 128. Preparation occurs off the UI thread; a pending or failed image leaves the rest of the widget usable.

ui.image{ image = "assets/cat.gif", w = 48, h = 48, radius = 8 }

ui.image

image: (props: ImageProps) -> Element,

ImageProps

export type ImageProps = Props & {
image: string | Resource,
thumbnail: number?,
playing: boolean?,
speed: number?,
}

Fields: image, thumbnail, playing, speed

ui.progress displays value as a fraction from 0 to 1. ui.slider uses the same normalized value; its onChange(ctx, event) receives event.value. Values are clamped to the displayed range. Convert percentages to fractions when rendering and back when sending a command.

ui.slider{ key = "volume", w = 180, h = 20,
value = (ctx.data.volume or 0) / 100, color = "accent",
onChange = function(ctx, event)
omniwm.run("volume set " .. tostring(math.floor((event.value or 0) * 100)))
end,
}

ui.progress

progress: (props: ProgressProps) -> Element,

ProgressProps

export type ProgressProps = Props & {
value: number,
color: Color?,
}

Fields: value, color

ui.slider

slider: (props: SliderProps) -> Element,

SliderProps

export type SliderProps = Props & {
value: number,
color: Color?,
onChange: Click?,
}

Fields: value, color, onChange

Give ui.input a stable key and explicit width and height in a bar, island, popout or drawer:

ui.input{
key = "query", text = ctx.state.query or "", placeholder = "Search",
w = 240, h = 28, maxLength = 256,
onCommit = function(ctx, event) ctx.state.query = event.text end,
onCancel = function(ctx) ctx.state.editing = false end,
}

AppKit owns selection, composition, clipboard and undo. While focused, the native editor keeps its text across renders. Once unfocused, it adopts the next declared text. focus = true requests focus when it changes to true; it does not repeatedly steal focus. focus = false releases it. onFocus receives event.focused.

Return commits a single-line field. With multiline = true, Return inserts a newline and Command + Return commits. Escape sends onCancel unless an IME is composing; it does not itself rewrite the value. Tab and Shift + Tab move among visible inputs. maxLength counts grapheme clusters, accepts 0–65,536 and defaults to 65,536. Composition can temporarily exceed the limit until committed.

onChange is optional. Pending changes for the same field are coalesced; commit, cancel and focus events keep their order. Omit onChange when only the final text matters, so typing needs no Luau callback. A shell focus session transfers between surfaces while retaining its previous-window return target.

Place editors in settled, untransformed regions. Their frames follow delivered layout and rectangular scroll clips. Arbitrary ancestor path masks, rotation, scale and animated layer geometry are not mirrored onto AppKit editors each frame. Surrounding shell content keeps its usual animations.

ui.input does not accept children or mask = true. Its onChange, onCommit, onCancel, and onFocus must be callbacks. Typography properties have the same meanings as on ui.text.

ui.input

input: (props: TextInputProps) -> Element,

TextInputProps

export type TextInputProps = Props & {
key: string,
text: string?,
placeholder: string?,
multiline: boolean?,
maxLength: number?,
focus: boolean?,
style: TextStyle?,
font: Font?,
size: number?,
weight: Weight?,
color: Color?,
onChange: InputHandler?,
onCommit: InputHandler?,
onCancel: InputHandler?,
onFocus: InputHandler?,
}

Fields: key, text, placeholder, multiline, maxLength, focus, style, font, size, weight, color, onChange, onCommit, onCancel, onFocus

ui.path draws a vector shape from SVG path data, without writing a shader: d takes the commands M L H V C S Q T A Z (lowercase for relative), fill, border and borderWidth colour it, and evenOdd = true switches the fill rule. With viewBox = { x, y, width, height } the path is stretched to the element’s size, so viewBox = { 0, 0, 24, 24 } with w and h draws a 24-point icon at any size; without w and h it takes the viewBox size, or fills the ui.stack it is in. A path with mask = true shapes its stack like a mask shader. When d changes and the path has animate, it morphs to the new shape, and enter = { d = "..." } morphs from that shape as the popout opens; both shapes need the same commands for a smooth morph.

ui.stack{ material = "thin", w = 120, h = 120,
ui.path{ viewBox = { 0, 0, 24, 24 }, mask = true,
d = "M12 2 L15 9 L22 9 L16.5 13.5 L18.5 21 L12 16.5 L5.5 21 L7.5 13.5 L2 9 L9 9 Z",
enter = { d = "M12 12 L12 12 L12 12 L12 12 L12 12 L12 12 L12 12 L12 12 L12 12 L12 12 Z" } } }

OmniWM ships liquid.metal, which grows a drop from the bar item into the popout:

function popout(ctx)
return ui.stack{ material = "clear", bridge = true,
ui.shader{ source = "liquid.metal", fps = 60 },
ui.column{ pad = 16, gap = 4,
ui.text{ text = ctx.data.clock, style = "bold" },
ui.text{ text = ctx.data.date },
},
}
end

ui.path

path: (props: PathProps) -> Element,

PathProps

export type PathProps = Props & {
d: string,
viewBox: { number }?,
evenOdd: boolean?,
}

Fields: d, viewBox, evenOdd

This block is copied verbatim from the shipped declarations. Use the symbol index to jump to individual constructors and all their types.

ui

declare ui: {
component: (props: ComponentProps) -> Element,
row: (props: Props) -> Element,
column: (props: Props) -> Element,
stack: (props: Props) -> Element,
box: (props: Props) -> Element,
text: (props: TextProps) -> Element,
input: (props: TextInputProps) -> Element,
symbol: (props: SymbolProps) -> Element,
icon: (props: IconProps) -> Element,
image: (props: ImageProps) -> Element,
progress: (props: ProgressProps) -> Element,
shader: (props: ShaderProps) -> Element,
path: (props: PathProps) -> Element,
slider: (props: SliderProps) -> Element,
popout: (ctx: Context) -> (),
}