Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Styling

Perry widgets support native styling properties that map to each platform’s styling system.

Coming from CSS

Perry’s layout model is closer to SwiftUI or Flutter than CSS. If you’re coming from web development, here’s how concepts translate:

CSSPerry
display: flex; flex-direction: columnVStack(spacing, [...])
display: flex; flex-direction: rowHStack(spacing, [...])
justify-contentstackSetDistribution(stack, mode) + Spacer()
align-itemsstackSetAlignment(stack, value)
position: absolutewidgetAddOverlay + widgetSetOverlayFrame
width: 100%widgetMatchParentWidth(widget)
padding: 10px 20pxsetEdgeInsets(10, 20, 10, 20)
gap: 16pxVStack(16, [...]) — first argument is the gap
CSS variables / design tokensperry-styling package (Theming)
opacitysetOpacity(value)
border-radiussetCornerRadius(value)

See Layout for full details on alignment, distribution, overlays, and split views.

Colors

import { Text, Button } from "perry/ui";

const label = Text("Colored text");
label.setColor("#FF0000");              // Text color (hex)
label.setBackgroundColor("#F0F0F0");    // Background color

Colors are specified as hex strings (#RRGGBB).

Fonts

const label = Text("Styled text");
label.setFontSize(24);                // Font size in points
label.setFontFamily("Menlo");         // Font family name

Use "monospaced" for the system monospaced font.

Corner Radius

const btn = Button("Rounded", () => {});
btn.setCornerRadius(12);

Borders

const widget = VStack(0, []);
widget.setBorderColor("#CCCCCC");
widget.setBorderWidth(1);

Padding and Insets

const stack = VStack(8, [Text("Padded content")]);
stack.setPadding(16);
stack.setEdgeInsets(10, 20, 10, 20); // top, right, bottom, left

Sizing

const widget = VStack(0, []);
widget.setWidth(300);
widget.setHeight(200);
widget.setFrame(0, 0, 300, 200);  // x, y, width, height

Opacity

const widget = Text("Semi-transparent");
widget.setOpacity(0.5); // 0.0 to 1.0

Background Gradient

const widget = VStack(0, []);
widget.setBackgroundGradient("#FF0000", "#0000FF"); // Start color, end color

Control Size

const btn = Button("Small", () => {});
btn.setControlSize(0); // 0=mini, 1=small, 2=regular, 3=large

macOS: Maps to NSControl.ControlSize. Other platforms may interpret differently.

Tooltips

const btn = Button("Hover me", () => {});
btn.setTooltip("Click to perform action");

macOS/Windows/Linux: Native tooltips. iOS/Android: No tooltip support. Web: HTML title attribute.

Enabled/Disabled

const btn = Button("Submit", () => {});
btn.setEnabled(false);  // Greys out and disables interaction

Complete Styling Example

// demonstrates: a styled counter card using the real free-function API
// docs: docs/src/ui/styling.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm

import {
    App,
    Text,
    Button,
    VStack,
    HStack,
    State,
    Spacer,
    textSetFontSize,
    textSetFontFamily,
    textSetColor,
    widgetSetBackgroundColor,
    widgetSetEdgeInsets,
    setCornerRadius,
} from "perry/ui"

// Note: widgetSetBorderColor / widgetSetBorderWidth are macOS/iOS/Windows
// only — perry-ui-gtk4 doesn't export them (GTK4 borders are CSS-driven).
// Omitted from this demo so it compiles everywhere.

const count = State(0)

const title = Text("Counter")
textSetFontSize(title, 28)
textSetColor(title, 0.1, 0.1, 0.1, 1.0)

const display = Text(`${count.value}`)
textSetFontSize(display, 48)
textSetFontFamily(display, "monospaced")
textSetColor(display, 0.0, 0.478, 1.0, 1.0)

const decBtn = Button("-", () => count.set(count.value - 1))
setCornerRadius(decBtn, 20)
widgetSetBackgroundColor(decBtn, 1.0, 0.231, 0.188, 1.0)

const incBtn = Button("+", () => count.set(count.value + 1))
setCornerRadius(incBtn, 20)
widgetSetBackgroundColor(incBtn, 0.204, 0.78, 0.349, 1.0)

const controls = HStack(8, [decBtn, Spacer(), incBtn])
widgetSetEdgeInsets(controls, 20, 20, 20, 20)

const container = VStack(16, [title, display, controls])
widgetSetEdgeInsets(container, 40, 40, 40, 40)
setCornerRadius(container, 16)
widgetSetBackgroundColor(container, 1.0, 1.0, 1.0, 1.0)

App({
    title: "Styled App",
    width: 400,
    height: 300,
    body: container,
})

Colors are RGBA floats in [0.0, 1.0]. Divide each hex byte by 255 to convert — 0xFF3B30 becomes (1.0, 0.231, 0.188, 1.0). Padding is four explicit sides (widgetSetEdgeInsets(w, top, left, bottom, right)), not a single value.

Composing Styles

Reduce repetition by creating helper functions:

import { VStackWithInsets, Text, widgetAddChild } from "perry/ui";

function card(children: any[]) {
  const c = VStackWithInsets(12, 16, 16, 16, 16);
  c.setCornerRadius(12);
  c.setBackgroundColor("#FFFFFF");
  c.setBorderColor("#E5E5E5");
  c.setBorderWidth(1);
  for (const child of children) widgetAddChild(c, child);
  return c;
}

// Usage
card([Text("Title"), Text("Body text")]);

For larger apps, use the perry-styling package to define design tokens in JSON and generate a typed theme file. See Theming for the full workflow.

Next Steps