First Native App
Perry compiles declarative TypeScript UI code to native platform widgets. No
Electron, no WebView — real AppKit on macOS, UIKit on iOS, GTK4 on Linux,
Win32 on Windows. Every example on this page is a real source file under
docs/examples/ that CI compiles and runs on every PR.
A Simple Counter
// demonstrates: minimal stateful UI — label + increment button
// docs: docs/src/ui/state.md
// platforms: macos, linux, windows
// targets: ios-simulator, visionos-simulator, tvos-simulator, watchos-simulator, android, web, wasm
import { App, VStack, Text, Button, State } from "perry/ui"
const count = State(0)
App({
title: "Counter",
width: 400,
height: 300,
body: VStack(16, [
Text(`Count: ${count.value}`),
Button("Increment", () => count.set(count.value + 1)),
]),
})
Compile and run:
perry counter.ts -o counter
./counter
A native window opens with a label and two buttons. Clicking “Increment” updates the count in real-time.
How It Works
App({ title, width, height, body })— Creates a native application window.bodyis the root widget.State(initialValue)— Creates reactive state..valuereads,.set(v)writes and triggers UI updates.VStack(spacing, [...])— Vertical stack layout (like SwiftUI’s VStack or CSS flexbox column). The first arg is the gap in points between children.Text(string)— A text label. Template literals referencing${state.value}bind reactively.Button(label, onClick)— A native button with a click handler.
A Todo App
// demonstrates: complete reactive todo app combining State, ForEach, and widget tree mutation
// docs: docs/src/ui/state.md
// platforms: macos, linux, windows
// targets: ios-simulator, tvos-simulator, watchos-simulator, web, wasm
import {
App,
Text,
Button,
TextField,
VStack,
HStack,
State,
ForEach,
Spacer,
Divider,
} from "perry/ui"
const todos = State<string[]>([])
const count = State(0)
const input = State("")
App({
title: "Todo App",
width: 480,
height: 600,
body: VStack(16, [
Text("My Todos"),
HStack(8, [
TextField("What needs to be done?", (value: string) => input.set(value)),
Button("Add", () => {
const text = input.value
if (text.length > 0) {
todos.set([...todos.value, text])
count.set(count.value + 1)
input.set("")
}
}),
]),
Divider(),
ForEach(count, (i: number) =>
HStack(8, [
Text(todos.value[i]),
Spacer(),
Button("Delete", () => {
todos.set(todos.value.filter((_, idx) => idx !== i))
count.set(count.value - 1)
}),
]),
),
Spacer(),
Text(`${count.value} items`),
]),
})
ForEach(count, render) iterates by index — keep an item array and a count
state in sync, then read items via array.value[i] inside the closure. See
State Management for the full pattern.
Cross-Platform
The same code runs on all 6 platforms:
# macOS (default)
perry app.ts -o app
./app
# iOS Simulator
perry app.ts -o app --target ios-simulator
# Web (compiles to WebAssembly + DOM bridge in a self-contained HTML file)
perry app.ts -o app --target web # alias: --target wasm
open app.html
# Other platforms
perry app.ts -o app --target windows
perry app.ts -o app --target linux
perry app.ts -o app --target android
Each target compiles to the platform’s native widget toolkit. See Platforms for details.
Adding Styling
Styling is applied via free functions that take the widget handle as their
first argument. Colors are RGBA floats in [0.0, 1.0] — divide a hex byte by
255 to convert (0x33 / 255 ≈ 0.2).
// demonstrates: counter from getting-started/first-app.md with styled widgets
// docs: docs/src/getting-started/first-app.md
// platforms: macos, linux, windows
// targets: ios-simulator, tvos-simulator, watchos-simulator, web, wasm
// run: false
import {
App, VStack, Text, Button, State,
textSetFontSize, textSetColor,
setCornerRadius, setPadding,
widgetSetBackgroundColor,
} from "perry/ui"
const count = State(0)
const label = Text(`Count: ${count.value}`)
textSetFontSize(label, 24)
textSetColor(label, 0.2, 0.2, 0.2, 1.0) // RGBA in [0,1] — same as #333333
const btn = Button("Increment", () => count.set(count.value + 1))
setCornerRadius(btn, 8)
widgetSetBackgroundColor(btn, 0.0, 0.478, 1.0, 1.0) // system blue
const stack = VStack(20, [label, btn])
setPadding(stack, 20, 20, 20, 20)
App({
title: "Styled Counter",
width: 400,
height: 300,
body: stack,
})
See Styling for all available style properties.
Next Steps
- Project Configuration — Set up
package.jsonfor Perry projects - UI Overview — Complete guide to Perry’s UI system
- Widgets Reference — All available widgets
- State Management — Reactive state and bindings