Documentation menu
Package

serez-ui

React-style UI library for Serez Code. Components, a transparent Virtual DOM, and hooks — the same code runs in the terminal (TUI) or in a real native window (GUI).

Install

sz install serez-ui

The model

You write screens as classes that extend Window (the root interface) and reusable pieces that extend Component — both return JSX from render(). State lives inthis fields; mutating it inside an event handler triggers a re-render through the Virtual DOM. JSX is written in .szx files and translated to plain .sz before running.

Tip: the public modifier on methods is optional in .szx — write render() { ... } and the translator adds public for you. Writing it explicitly works too; both styles coexist.

Quick start

A counter, rendered in a real native window:

import "serez-ui"

class Counter:Window {
    public Counter() {
        super()
        this.count = 0
    }

    public render() {
        return (
            <div>
                <h1>Counter</h1>
                <hr />
                <h2>{this.count}</h2>
                <Button onClick={() => { this.count = this.count + 1 }}>Increment</Button>
                <Button onClick={() => { this.count = 0 }}>Reset</Button>
            </div>
        )
    }
}

let app = new Counter()
app.runGui("Counter", 520, 420)   // native window — quit with Esc or the close button

Building .szx files

JSX lives in .szx files. Run them directly — the runtime translates the JSX to .sz and runs it (and opens the UI) in one step:

sz apps/counter.szx

The primitive form: h()

JSX is sugar. The translator turns every tag into a call to one primitive function, h(tag, props, children) — "hyperscript". So <div class="box">hi</div> becomes h("div", [["class", "box"]], ["hi"]): a tag string, a list of [name, value] prop pairs, and a list of children.

You can write that form by hand — it is valid. The counter above builds the same tree with no JSX at all:

public any render() {
    return h("div", [], [
        h("h1", [], ["Count: " + this.count]),
        h("Button", [["onClick", () => { this.count = this.count + 1 }]], ["+1"])
    ])
}
It is the same thing underneath, so the primitive form is always an option — handy in a plain .sz file (which never runs the JSX translator) or when you build nodes programmatically. It is the form you will see in the .szs reference, whose example is a plain .sz file.

Built-in components

Structure uses primitive HTML-like tags the renderer draws directly (div, h1, h2, h3, p, span, hr, ul, li, section, form). Block text (h1/h2/h3/p/span/li/Label) word-wraps to the available width and reflows on resize. For layout, Row places its children side by side (each at its content width, with a gap) — and stacks them vertically when they no longer fit (set wrap={false} to force a single row) — while Col stacks them vertically. For interaction, serez-ui ships 24 built-in components:

ComponentKey propsNotes
ButtononClick, disabledText is the children · Enter/Space activates when focused
Inputvalue, placeholder, type, onChange, onSubmit, disabledOne line · positionable caret, type="password" masks, Enter fires onSubmit
Textareavalue, placeholder, rows, onChange, disabledMulti-line · caret + vertical scroll, Enter inserts a newline
Selectvalue, options, onChange, disabledClick or ←→ cycles options
Dropdownvalue, options, onChange, disabledReal drop list · click/Enter opens, ↑↓ navigate, Enter picks
Checkboxchecked, label, onChange, disabledClick or Space toggles
Switch / Togglechecked, label, onChange, disabledOn/off pill switch (same semantics as Checkbox)
RadioGroupvalue, options, onChange, disabledOne choice · click an option or ↑↓ to move
Slidervalue, min, max, step, onChange, disabledClick the track or ←→ to change
Tabstabs, active, onChange, disabledControlled tab strip — you render the content per active · click or ←→
Collapsible / Accordiontitle, open, onToggle, disabledCollapsible section — a header (chevron) that shows/hides its children · click or Enter/Space
ProgressBarvalue, max, labelNon-interactive (skipped by focus)
Chartdata, type (line/area/bar), height, color, min, max, dotsPlots a numeric series (core vector primitives); sparkline in TUI
LabelCaption text (children); non-interactive
Linkhref, onClick, disabledUnderlined accent · click or Enter activates
Imagesrc, bytes, width, height, alpha, altRaster (PNG/JPG) from a file or bytes in memory (a fetched image); scales to width/height
FileInputonChange, value, label, filterName, exts, save"Choose file…" button → native file dialog; shows the picked name
DropZoneonDrop, label, heightFile drag-drop area; highlights while files hover, onDrop(paths) on drop
Tablecolumns, rowsRead-only grid (aligned cells, header row)
Modalopen, titleWhen open, dims the background and centers a box with the children on top
TooltiptipWraps a child; shows a small box next to the cursor on hover
Toastmessage, kindTransient banner (info/success/warn/error); auto-dismiss from onFrame()
<Input value={this.name} placeholder="your name" onChange={(v) => { this.name = v }} />
<Textarea value={this.bio} rows={4} onChange={(v) => { this.bio = v }} />
<Select value={this.mode} options={["fast", "normal", "slow"]} onChange={(v) => { this.mode = v }} />
<Dropdown value={this.lang} options={["es", "en", "fr"]} onChange={(v) => { this.lang = v }} />
<Checkbox checked={this.agreed} label="I agree" onChange={(b) => { this.agreed = b }} />
<RadioGroup value={this.size} options={["S", "M", "L"]} onChange={(v) => { this.size = v }} />
<Slider value={this.vol} min={0} max={100} step={5} onChange={(v) => { this.vol = v }} />
<ProgressBar value={this.vol} max={100} label="level" />
<Switch checked={this.dark} label="Dark mode" onChange={(b) => { this.dark = b }} />
<Chart data={[3, 7, 4, 9, 6, 11, 8]} type="area" height={140} dots={true} />
<Row>
    <Button onClick={save} disabled={!this.canSave}>Save</Button>
    <Button onClick={clear}>Clear</Button>
</Row>

// Tabs draw the strip; you render the panel for the active index:
<Tabs tabs={["Info", "Config", "Logs"]} active={this.tab} onChange={(i) => { this.tab = i }} />
{ this.tab == 0 ? <Info /> : this.tab == 1 ? <Config /> : <Logs /> }

// Collapsible: a header that shows/hides its children (stack a few for an accordion):
<Collapsible title="Details" open={this.open} onToggle={(o) => { this.open = o }}>
    <p>Hidden until expanded.</p>
</Collapsible>

// Files (both need the File permission to read the paths):
<FileInput exts="json" filterName="JSON" onChange={(p) => { this.path = p }} />
<DropZone label="Drop files here" onDrop={(paths) => { this.files = paths }} />

Focus & keyboard (GUI)

Every interactive component is focusable and gets a focus index in render order. The focused element shows a ring (text fields highlight their border and draw the caret). This navigation is built in — you do not wire it up:

KeyAction
Tab / Shift+TabMove focus to the next / previous component
clickFocus that component (and, in a text field, place the caret under the cursor)
Home EndMove the caret (text fields) · change value (Select / Slider) · switch tab (Tabs)
Backspace / DeleteDelete before / at the caret (auto-repeat when held)
Enter / SpaceActivate the focused Button / Link / Checkbox / Switch / Dropdown · open the dialog (FileInput)
EnterNewline in a Textarea · onSubmit in an Input
Navigate options in an open Dropdown / a RadioGroup
EscClose the window

Text is drawn with real glyphs on a monospace grid, and typing goes through your OS keyboard layout and IME — so accents, ñ and Unicode type straight into an Input or Textarea. A CJK IME composition in progress is drawn underlined at the caret, and when the window loses OS focus the caret stops blinking (a background window costs ~0 CPU).

OS events (drag-drop, gestures)

The GUI surfaces a few window-level OS events as optional Window overrides (all default to no-op; return true to request a redraw). The DropZone and FileInput components cover the common cases, but you can also handle them directly:

public bool onFilesDropped(any paths) {   // files dropped on the window (needs File perm to read)
    this.attached = paths
    return true
}
public bool onFilesHovered(any paths) { ... }   // files dragged over the window (before dropping)
public bool onPinch(any delta)        { ... }   // trackpad pinch/zoom (delta > 0 in, < 0 out)
public bool onTouch(any touches)      { ... }   // touchscreen points: flat [id, phase, x, y, ...]

app.hoveredFiles() returns the paths currently being dragged over the window (or []), so a component can read it from render() to highlight a drop target — which is what DropZone does.

Reusable components

Beyond Window (the root interface), extend Component to build your own reusable pieces. A component receives props (data passed to it) and children (content between its tags) — read them with plain dot access:

class TaskRow:Component {
    public TaskRow() { super() }

    public render() {
        let mark = "[ ] "
        if (this.props.done) { mark = "[x] " }
        return (
            <li>{mark + this.props.text}</li>
        )
    }
}

// use it like any tag — props are attributes, children is the content:
<TaskRow text="buy milk" done={true} />
Note: you write this.props.text with a dot; the translator rewrites it to the dict access serez-code uses under the hood. Content between tags arrives as this.children.

Lists with .map()

Render a list by mapping an array to nodes — no manual loops, no h(...) calls. The callback can return any tag: an HTML element, a fragment <>...</>, or another component:

public render() {
    let tasks = this.props.tasks
    return (
        <ul>
        {
            tasks.map(fn (t) {
                return (
                    <TaskRow text={t} done={false} />
                )
            })
        }
        </ul>
    )
}

Fragments group children without adding a wrapper element — handy when the callback returns bare values:

items.map(fn (x) {
    return (
        <>{x}</>
    )
})

TUI vs GUI

The same component runs in two renderers, but they are not interchangeable for input — each has its own event loop, serez.json permission, and interaction model:

TUI (terminal)GUI (window)
Startapp.runTui()app.runGui(title, w, h)
Permission"Terminal""Gui"
Interactionraw keyboard via onKey(evt)click, typing + built-in focus/keyboard nav
App onKey(evt)✅ dispatched✗ not dispatched — components handle keys themselves
// The same component runs itself in either target — the event loop is a
// method on your component (app is your top-level variable):

// Terminal (TUI) — needs "Terminal" permission; onKey drives navigation
app.runTui()                       // quit with q

// Native window (GUI) — needs "Gui" permission; interact via Buttons/Inputs
app.runGui("My App", 560, 460)     // quit with Esc or the close button

For a full worked example of each, see the Build a UI (TUI / GUI) tutorial.

Resizing & width bounds

The GUI reflows on window resize (autosize): full-width controls (Input, Select, Textarea, Dropdown, Slider, ProgressBar, hr) stretch and shrink to the window, while buttons and labels keep their content size. A Row whose children no longer fit stacks them vertically on its own (no config), and content taller than the window scrolls with the mouse wheel (a scrollbar appears on the right). Bound the content width — and the layout centers itself when the max-width is narrower than the window:

app.setMaxWidth(720)   // content never wider than 720px (centered on big screens)
app.setMinWidth(360)   // ...and never narrower than 360px
app.runGui("My App", 900, 600)

Pass 0 (the default) for no limit. For width-driven styling and layout — including stacking a Row when narrow — see CSS media queries below.

Breakpoints in render()

For structure that changes with size — not just styling — read this.viewportWidth() (live px) or this.breakpoint() ("sm" / "md" / "lg") inside render(). The GUI re-runs render() on resize, so you can return a different tree per breakpoint:

public any render() {
    if (this.breakpoint() == "sm") {
        return <Button>Menu</Button>          // phone: a single button
    }
    return (                                   // desktop: a row of links
        <Row>
            <Button>Home</Button>
            <Button>Profile</Button>
            <Button>Settings</Button>
        </Row>
    )
}

Thresholds default to 600 / 960; change them with app.setBreakpoints(smMax, mdMax) before runGui.

CSS with logic (.szs)

Style with a CSS dialect that supports reactive conditions. A selector can carry a condition evaluated against the state exposed by styleVars(). This is a quick tour — see the full .szs reference for every property, condition and the responsive variables:

/* counter.szs */
:import {
    count: count;
}

body (count == 0) { background-color: #0f172a; }
body (count != 0) { background-color: #14532d; }

h1     { color: #ffd166; }
Button { background-color: #2563eb; color: #ffffff; }

Attach the stylesheet to your component before starting:

app.useStylesheet(parseCss(File.read("apps/counter.szs")))
app.runGui("Counter", 520, 420)

Responsive — media queries

The current width and height (px) are always available as condition variables (no :import needed), so a stylesheet can adapt the UI to the window size — and it reflows live on resize:

body (width < 600)  { background-color: #1e1b4b; }   /* phone-ish */
body (width >= 600) { background-color: #0f172a; }

Row (width < 600)   { direction: column; }            /* stack the row when narrow */

h1  (width < 600)   { font-scale: 2; }                /* smaller heading when small */
h1  (width >= 960)  { font-scale: 4; }

Beyond colors, the sheet understands direction: column (lay a Row out vertically), font-scale: N (integer text-size multiplier for h1/h2/h3/p/span/li) and white-space: nowrap(keep a tag's text on one line — it may clip — instead of word-wrapping).

Secondary Windows (Panels)

Since serez-ui 2.3.0, the GUI renderer runs on top of a retained-mode scene graph, drastically reducing CPU usage. It also adds native support for opening and managing secondary OS windows (referred to as panels).

To use panels, open them using this.openPanel(title, w, h) from within your main application window, and override the renderPanel(id) method in your class to define the UI tree of each panel:

import "serez-ui"

class MyApp:Window {
    public MyApp() {
        super()
        this.message = "Hello from panel"
        this.secondaryWindowId = -1
    }

    public render() {
        return (
            <div>
                <h1>Main Window</h1>
                <Button onClick={() => {
                    if (this.secondaryWindowId == -1) {
                        this.secondaryWindowId = this.openPanel("Controls", 300, 200)
                    }
                }}>Open Secondary Panel</Button>
            </div>
        )
    }

    // Override renderPanel to draw the secondary window's tree
    public renderPanel(int id) {
        if (id == this.secondaryWindowId) {
            return (
                <div>
                    <h2>Panel Controls</h2>
                    <p>{this.message}</p>
                    <Button onClick={() => {
                        this.message = "Button clicked in Panel!"
                    }}>Update Main State</Button>
                    <Button onClick={() => {
                        this.closePanel(this.secondaryWindowId)
                        this.secondaryWindowId = -1
                    }}>Close Panel</Button>
                </div>
            )
        }
        return null
    }
}

let app = new MyApp()
app.runGui("Main App", 600, 400)

Preserved State Identity:click handlers inside a panel's VDOM can directly access and modify the properties of the parent Window class. A click in a secondary panel that updates state automatically re-renders the main window.

MethodReturnsWhat it does
this.openPanel(title, w, h)intOpens a secondary window; returns its id. Call from the main window (e.g. in a click handler or onFrame). Requires runGui running.
this.closePanel(id)Closes the panel with that id.
this.panelCount()intNumber of panels currently open.
renderPanel(id)vdomOverride in your class — returns that panel's UI tree, or null for an empty panel. Called once per frame for each panel.
v1 limitations. Panels route Button / Link clicks— their handlers mutate the main app's state, which re-renders both windows. Text editing is not wired in panels yet: keep Input / Textarea in the main window for now (a panel is best for controls, dashboards, or read-only views). Closing a panel with the OS window × removes it automatically and the main loop keeps running.
Panels are the serez-ui layer over the core's multi-window Gui API (Gui.openWindow / selectWindow / closeWindow). Reach for that only when you draw raw Gui primitives instead of components.

Native renderer (experimental)

serez-ui 4.3.0 can hand style resolution, layout and painting to the core's primitives engine (Gui.renderTree, requires core ≥ 9.2): every component is lowered to HTML-like primitives and the whole layout + CSS walk runs in Rust instead of interpreted code — measured ~1000× faster on that phase for app-sized trees. All 24 built-in components work under this path (clicks, focus order, overlays and text editing included), and your app code does not change:

let app = new MyApp()
app.useNativeRenderer(true)   // opt-in — call before runGui()
app.runGui("My App", 800, 600)
Opt-in for now. The flag is off by default while the path matures; the classic interpreted renderer stays the default and both render the same UI from the same components and .szs sheet. Current gaps of the native path: descendant CSS selectors (.a .b, used by some focus rings) and continuous Slider drag (click-to-set and keyboard work).

API surface

ExportWhat it is
WindowBase class for the root interface
ComponentBase class for reusable components (props + children)
h / hFrag / VNodeHyperscript, fragments + Virtual DOM node
diff / PatchVirtual DOM diffing
useState / useEffect / memoHooks
app.runTui() / app.runGui(title, w, h)Window methods — run the app in the terminal / a native window
app.useStylesheet(sheet)Window method — attach a .szs stylesheet (call before runGui)
app.setMaxWidth(px) / app.setMinWidth(px)Window methods — clamp the GUI content width (0 = no limit; centers under max-width)
app.viewportWidth() / app.breakpoint()Window methods — live viewport width (px) / current breakpoint (sm/md/lg) for a responsive render()
app.setBreakpoints(smMax, mdMax)Window method — set the sm|md and md|lg width thresholds (default 600 / 960)
app.onFrame()Window override — per-frame hook; return true to redraw
app.onFilesDropped / onFilesHovered / onPinch / onTouchWindow overrides — OS file drag-drop and trackpad/touch gestures
app.hoveredFiles()Window method — paths being dragged over the window now ([] if none)
app.openPanel(title, w, h)Window method — Open secondary panel window → ID
app.closePanel(id)Window method — Close secondary panel window
app.panelCount()Window method — Returns total number of active panels
app.renderPanel(id)Window override — Define rendering tree for secondary panel ID
app.useNativeRenderer(b)Window method — render through the core's primitives engine (experimental, core ≥ 9.2; call before runGui)
Renderer / GuiRendererTUI / GUI renderers (used internally by the run methods)
parseCss.szs stylesheet parser

Packaging

Ship a serez-ui app as a self-contained installer (the runtime travels inside, no Serez Code needed on the target) with serez-pack.