Perry provides native widgets that map to each platform's native controls.
Every example on this page is a real runnable program verified by CI
(scripts/run_doc_tests.sh) — the snippet you read is the same source that's
compiled and launched.
Most widget operations use free functions. A widget is a 64-bit opaque
handle; pass it into helpers like textSetFontSize(widget, 18) rather than
calling widget.setFontSize(18). The small method surface declared by
WidgetMethods is cross-backend too: it includes animations plus
parent.addChild(child) and parent.removeAllChildren() compatibility aliases
for widgetAddChild and widgetClearChildren.
Displays read-only text.
{{#include ../../examples/ui/widgets/text.ts}}Color is RGBA with each channel in [0.0, 1.0] — divide a hex byte by 255
(0x33 / 255 ≈ 0.2).
Helpers: textSetString, textSetFontSize, textSetFontWeight,
textSetFontFamily, textSetColor, textSetWraps, textSetSelectable.
Text widgets inside template literals with state.value update automatically
— perry detects the state read and rewires the widget to re-render on change.
See State Management.
A clickable button.
{{#include ../../examples/ui/widgets/button.ts}}Helpers: buttonSetTitle, buttonSetBordered, buttonSetImage
(SF Symbol name on macOS/iOS), buttonSetImagePosition,
buttonSetContentTintColor, buttonSetTextColor, widgetSetEnabled.
An editable single-line text input.
{{#include ../../examples/ui/widgets/textfield.ts}}TextField(placeholder, onChange) fires onChange as the user types. Pair
with stateBindTextfield(state, field) for two-way binding so programmatic
state.set(…) also updates the visible text.
Helpers: textfieldSetString, textfieldSetFontSize,
textfieldSetTextColor, textfieldSetBackgroundColor,
textfieldSetBorderless, textfieldSetOnSubmit, textfieldSetOnFocus,
textfieldSetNextKeyView.
A password input — identical signature to TextField, but text is masked.
{{#include ../../examples/ui/widgets/secure_field.ts}}A boolean on/off switch.
{{#include ../../examples/ui/widgets/toggle.ts}}A numeric slider.
{{#include ../../examples/ui/widgets/slider.ts}}Slider(min, max, onChange) — onChange fires on every drag. Use
stateBindSlider(state, slider) for two-way binding.
A dropdown selection control. Items are added with pickerAddItem.
{{#include ../../examples/ui/widgets/picker.ts}}Two distinct constructors:
ImageFile(path)— image from a file pathImageSymbol(name)— SF Symbol glyph name (macOS/iOS only)
{{#include ../../examples/ui/widgets/image_symbol.ts}}Use widgetSetWidth(img, N) / widgetSetHeight(img, N) to size the image.
An indeterminate or determinate progress indicator.
{{#include ../../examples/ui/widgets/progressview.ts}}A multi-line text input. Same (placeholder, onChange) signature as
TextField but renders as a multi-line box.
{{#include ../../examples/ui/widgets/textarea.ts}}Helpers: textareaSetString.
Group controls into labelled sections. Perry has no Form() widget — use a
VStack of Section(title)s and attach children via widgetAddChild.
{{#include ../../examples/ui/widgets/sections.ts}}5-tab bottom bar with icon + label + badge per tab. onSelect(index)
fires when the user taps; bottomNavSetSelected is the programmatic
counterpart and does NOT fire onSelect.
import {
BottomNavigation,
bottomNavAddItem,
bottomNavSetBadge,
} from "perry/ui";
const bar = BottomNavigation((index) => {
console.log("tab:", index);
});
bottomNavAddItem(bar, "house", "Home");
bottomNavAddItem(bar, "magnifyingglass", "Search");
bottomNavAddItem(bar, "bell", "Activity");
bottomNavSetBadge(bar, 2, "5");Real on macOS (NSStackView + NSButton strip with SF Symbol icons),
iOS (UITabBar), Android (custom LinearLayout strip with badge
overlay), and GTK4 (GtkBox + Adwaita CSS). Stub on Windows, tvOS,
visionOS, watchOS.
Swipeable, paging carousel of images. Local file paths load synchronously; HTTP/HTTPS URLs are fetched on a background queue and applied on the main thread.
import { ImageGallery, imageGalleryAddImage } from "perry/ui";
const gallery = ImageGallery((idx) => console.log("page:", idx));
imageGalleryAddImage(gallery, "/photos/01.jpg", "Hero shot");
imageGalleryAddImage(gallery, "https://cdn.example/photo2.jpg", "Wide angle");Real on macOS (NSScrollView paging), iOS (UIScrollView with
scrollViewDidEndDecelerating), Android (HorizontalScrollView), GTK4
(GtkScrolledWindow + GtkPicture). Stub on Windows, tvOS, visionOS,
watchOS.
Available on ScrollView and LazyVStack. The onPull callback fires
once when the user pulls past the threshold; call
scrollviewEndRefreshing (or lazyvstackEndRefreshing) when your async
fetch settles to dismiss the spinner.
import {
ScrollView,
scrollviewSetRefreshControl,
scrollviewEndRefreshing,
} from "perry/ui";
const scroll = ScrollView();
scrollviewSetRefreshControl(scroll, async () => {
await refreshFeed();
scrollviewEndRefreshing(scroll);
});Real on iOS (UIRefreshControl). The macOS / Android / GTK4 / Windows
desktops have no native pull-to-refresh idiom — they're documented
no-ops.
Fires once when the user scrolls past thresholdPx (or thresholdItems
for LazyVStack) from the bottom; re-arms after the user scrolls back
up past the threshold so a single fetch is queued at a time.
import { ScrollView, scrollviewSetScrollEndCallback } from "perry/ui";
const scroll = ScrollView();
scrollviewSetScrollEndCallback(
scroll,
() => loadMore(),
200, // threshold in pixels from the bottom
);Real on every platform that has a scroll view: macOS
(NSViewBoundsDidChangeNotification), iOS
(UIScrollViewDelegate.scrollViewDidScroll), Android
(View.OnScrollChangeListener), GTK4 (GtkAdjustment::value-changed),
Windows (WM_VSCROLL / WM_MOUSEWHEEL).
These exist only on specific platforms and aren't verified by the cross-platform doc-tests:
Table(rows, cols, renderer)— macOS only. Now supportstableSetSortColumn,tableSetFilterText, and multi-select since v0.5.636 (#473).QRCode(data, size)— macOS only. Renders a QR code.Canvas(width, height, draw)— all desktop platforms. A drawing surface; see Canvas.CameraView()— iOS only (other platforms planned). See Camera.
Editable text field with a filterable dropdown of suggestions. macOS
uses NSComboBox with as-you-type completion; other platforms stub the
FFI today (the field falls back to a plain editable field).
import { Combobox, comboboxAddItem, comboboxGetValue } from "perry/ui";
const combo = Combobox("", (v) => console.log("picked:", v));
comboboxAddItem(combo, "apple");
comboboxAddItem(combo, "apricot");
comboboxAddItem(combo, "avocado");Hierarchical disclosure list. Build the topology bottom-up via TreeNode
treeNodeAddChild, then mount it viaTreeView. macOS usesNSOutlineView; other platforms stub.
import { TreeNode, treeNodeAddChild, TreeView } from "perry/ui";
const dox = TreeNode("docs", "Documents");
treeNodeAddChild(dox, TreeNode("doc-1", "Resume.pdf"));
treeNodeAddChild(dox, TreeNode("doc-2", "Cover Letter.pdf"));
const root = TreeNode("root", "Files");
treeNodeAddChild(root, dox);
const tree = TreeView(root, (id) => console.log("selected:", id));Month-grid date picker. macOS uses NSDatePicker in graphical style;
other platforms stub. onChange receives the selected date as an ISO
yyyy-MM-dd string.
import { Calendar, calendarGetSelectedDate } from "perry/ui";
const cal = Calendar(2026, 5, (iso) => console.log("date:", iso));Compact field-style date picker — the space-saving complement to the
month-grid Calendar. Each platform uses its native compact date control:
macOS NSDatePicker (text-field-and-stepper), iOS / visionOS
UIDatePicker (.compact), Windows SysDateTimePick32, Android
android.widget.DatePicker. GTK4 has no native compact date field, so it
reuses GtkCalendar; tvOS / watchOS stub. onChange receives the selected
date as an ISO yyyy-MM-dd string.
import { DatePicker, datePickerGetSelectedDate } from "perry/ui";
const picker = DatePicker(2026, 5, (iso) => console.log("date:", iso));Line / bar / pie via CoreGraphics on macOS. kind is 0=line, 1=bar,
2=pie. Apple Charts framework / SwiftUI Charts integration on iOS 16+
is a follow-up.
import { Chart, chartAddDataPoint, chartSetTitle } from "perry/ui";
const chart = Chart(0, 600, 400);
chartSetTitle(chart, "Visits");
chartAddDataPoint(chart, "Mon", 12);
chartAddDataPoint(chart, "Tue", 18);
chartAddDataPoint(chart, "Wed", 9);⌘K-style fuzzy command launcher. macOS shows a floating NSPanel; other
platforms stub. Bind commandPaletteShow() to ⌘K via
addKeyboardShortcut to wire the default hotkey.
import {
commandPaletteRegister,
commandPaletteShow,
} from "perry/ui";
commandPaletteRegister("save", "Save", "⌘S", () => save());
commandPaletteRegister("export", "Export PDF", "", () => exportPdf());
// then:
commandPaletteShow();Wraps MKMapView on macOS / iOS / visionOS / tvOS, libshumate on GTK4,
Google Maps SDK on Android (requires API key in
AndroidManifest.xml), and the SwiftUI Map view on watchOS. Windows
remains a stub (WinUI MapControl needs XAML Islands integration).
import {
MapView,
mapViewSetRegion,
mapViewAddPin,
mapViewSetMapType,
} from "perry/ui";
const map = MapView(800, 600);
mapViewSetRegion(map, 37.7749, -122.4194, 0.05, 0.05);
mapViewAddPin(map, 37.7749, -122.4194, "San Francisco");
mapViewSetMapType(map, 1); // 0=standard, 1=satellite, 2=hybridPDFView from PDFKit on macOS / iOS / visionOS. pdfViewLoadFile
returns 1 on success, 0 on failure.
import {
PdfView,
pdfViewLoadFile,
pdfViewGetPageCount,
} from "perry/ui";
const pdf = PdfView(800, 600);
if (pdfViewLoadFile(pdf, "/tmp/report.pdf")) {
console.log("pages:", pdfViewGetPageCount(pdf));
}NSTextView with NSAttributedString storage on macOS. Plain-text and
HTML round-trip cover persistence; richTextToggleBold /
ToggleItalic / ToggleUnderline cover inline formatting via
NSResponder actions.
import {
RichTextEditor,
richTextSetHtml,
richTextGetHtml,
richTextToggleBold,
} from "perry/ui";
const editor = RichTextEditor(600, 400, (text) => console.log(text));
richTextSetHtml(editor, "<p>Hello <b>world</b></p>");
richTextToggleBold(editor);widgetSetRichTooltip(widget, content, hoverDelayMs) — like
widgetSetTooltip but the tooltip content is itself a Perry widget.
macOS uses NSPanel + NSTrackingArea; other platforms stub. For
plain-text tooltips with VoiceOver / a11y support, prefer the simpler
widgetSetTooltip.
WebView({ url, allowedDomains?, onShouldNavigate?, ... }) embeds a
real browser engine — WKWebView on Apple, WebView2 on Windows,
WebKitGTK 6.0 on Linux, android.webkit.WebView on Android,
sandboxed <iframe> on web. See WebView for the full
OAuth / callback-interception pattern and the per-platform notes.
These are linked from their own pages where richer examples exist.
Every widget handle accepts these:
| Helper | Description |
|---|---|
widgetSetWidth(w, n) / widgetSetHeight(w, n) |
Explicit size in points |
widgetSetBackgroundColor(w, r, g, b, a) |
RGBA in [0, 1] |
setCornerRadius(w, r) |
Rounded corners in points |
widgetSetOpacity(w, alpha) |
Opacity in [0, 1] |
widgetSetEnabled(w, flag) |
0 disables, 1 enables |
widgetSetHidden(w, flag) |
0 visible, 1 hidden |
widgetSetTooltip(w, text) |
Tooltip on hover (desktop only) |
widgetSetOnClick(w, cb) |
Click handler |
widgetSetOnHover(w, cb) |
Hover enter/leave (desktop only) |
widgetSetOnDoubleClick(w, cb) |
Double-click handler |
widgetSetEdgeInsets(w, top, left, bottom, right) |
Padding around contents |
widgetSetBorderColor(w, r, g, b, a) / widgetSetBorderWidth(w, n) |
Border |
widgetAddChild(parent, child) |
Attach a child to a container |
widgetSetContextMenu(w, menu) |
Right-click menu |
See Styling and Events for deeper coverage.
- Layout — Arranging widgets with stacks and containers
- Styling — Colors, fonts, borders
- State Management — Reactive bindings