# helder.build > Helder is a high-performance UI and graphics stack for native apps on desktop and mobile, and for the web. Its studio, at https://helder.build/studio/, runs entirely in the user's browser (recent Chrome or Edge, WebGPU): it draws design files at once, builds Haxe apps with a compiler in the page, and keeps projects in the browser's storage. No account. An agent makes the files; the user opens a link and sees them live. This file tells an agent how to build an app in helder.build that works. It is generated from helder's source (commit 01e33462cd): https://github.com/benmerckx/helder ## How an agent works with it Two ways in, the same result: a link the user opens in their browser. ### 1. Over MCP (best: you see what the user sees) Add the relay, an MCP server over Streamable HTTP, no account: claude mcp add --transport http helder https://helder.build/mcp Any other MCP client: a remote (HTTP) server at `https://helder.build/mcp`, e.g. {"mcpServers": {"helder": {"type": "http", "url": "https://helder.build/mcp"}}} Tools: - `project.create {name?, files?}`: a draft from your files (`{path: text}`); answers `{id, url}`. Give the user the `url` (`https://helder.build/studio/#draft=`) and ask them to open it. - `project.read {id, path?}`: a file as it is now (the user's own edits in the studio come back to the draft), or the list of files and whether a studio tab has the draft open. - `project.patch {id, path, edits: [{old, new}]}` or `{id, path, text}`: each `old` must occur exactly once in the file; `text` replaces the file or makes a new one. The open tab applies it live, through its history (the user can undo it), and answers what happened. - `project.status {id}`: whether a tab has it open, and what the tab reported after the last change: `diagnostics`, each screen's node tree (tags, boxes, texts, colours, `file:line`) and a small PNG of each screen. `stale: true` means the tab has not reported on the latest version yet: wait a moment and ask again. The relay only stores and relays text: the user's browser runs, checks and draws everything, so `project.status` is empty until the user has opened the link. The loop: create, give the link, patch, check `project.status` (its diagnostics and PNGs), and only then tell the user it is done. Limits: a draft is at most 1 MB of text and 200 files (paths relative, no dot directories); it expires 30 days after its last change; calls are rate-limited per IP (240 calls and 10 drafts a minute). ### 2. Without MCP: a link you make yourself https://helder.build/studio/#import= `` is the JSON `{"name": "my-app", "files": {"app.helder": "..."}}`, in one of three forms: - percent-encoded JSON as it is (`encodeURIComponent(json)`): the easiest to write without running code; - base64url of the JSON's UTF-8 bytes (starts with `ey`); - `z.` and base64url of the JSON deflated (deflate-raw, no zlib header): the shortest. Python: `base64.urlsafe_b64encode(zlib.compress(data, 9)[2:-4]).rstrip(b'=')`; Node: `zlib.deflateRawSync(data).toString('base64url')`. The studio opens it as a new project in the browser's storage. Nothing is sent to a server (the part after `#` stays in the browser). Limits: 1 MB and 200 files after decoding; the whole URL at most 2 MB (Chrome's limit); links pasted into chats and mail are safest under about 8 KB, so prefer the `z.` form, and MCP for anything larger. The link is not kept in sync: for changes, make a new link (it opens as a new project). ## What to write: start with a design file A new helder.build project is one design file, `app.helder`. Design files are drawn by the studio at once, nothing built, so they are the safest thing to make and the fastest to see. Write the screens as design files first; add Haxe code only when something must run (state, input, logic). A design file (`*.helder`) is helder's markup alone: - the root is ``; its children are ``s; - a `` is a screen on the canvas: `name` (unique in the file), its size in points (a phone is 390 by 844), `canvas:at="x y"` its place (put frames side by side: `0 0`, `430 0`, `860 0`...); its other attributes style it like a `view`; - `...` and `canvas:*` attributes are for the editor and are not drawn; - inside a frame: the elements and attributes below, with literal values only: strings (`color="#111827"`, `padding="16 24"`) and numbers (`font-size={32}`). No code in a design file: an expression (`{items.length}`), a component, an unknown element or a value its attribute rejects is outlined in red and reported in `project.status`'s diagnostics; - a frame's screen is named `#` (`app.helder#Home`); - layout is flexbox (a `view` stacks its children in a column; `direction="row"` for a row; `row-gap`, `column-gap`, `padding`, `align-items`, `justify`, `grow`), sizes in points; - text goes in ``; a `view` holds no bare text; - images by path (``) are not drawn in the browser yet: use shapes (views with `background` and `radius`) and `` (lucide's icons by name). The studio's new project, `app.helder`, a good start: ``` A design file: drawn at once, nothing built. Select, move, resize; the inspector edits it. Hello A first screen. Edit the text, the colours, the spacing. Start Later Next A card Frames are screens. Add one beside this. ``` helder's design example (examples/design/app.helder): ``` The list as it opens: three items, one done. todos What needs to be done? Add Sketch the home screen Pick the colours Write the list in code 2 items left todos Nothing to do Add an item to get started. ``` ## Elements and attributes (helder.ui) From helder.ui's README (src/helder/ui/README.md, "Markup"): Intrinsic elements: `view` (a box), `text` (a run of text), `scroll` (a box that scrolls), `image` (an `ImageNode`: `src={image}` or `src={svg}`, `fit="cover"`, `object-position="left top"`). Inside a `text`, elements are spans of rich text: `span`, `b`, `i` or `text` again, each with its own style (colour, weight, size...) and handlers (a link: ``); lines are as high as CSS makes them, their spans on one baseline. Attributes: - any Style property, in camelCase or kebab-case, with values as CSS writes them: `direction="row"`, `justify="space-between"`, `padding="8 12"`, `width="50%"`, `background="#3b82f6"`, `radius={6}`; `style={...}` takes a typed `StyleProps` object; - events: `onClick`, `onPointerDown`, `onPointerUp`, `onPointerMove`, `onPointerEnter`, `onPointerLeave`, `onPointerCancel`, `onLostPointerCapture`, `onWheel`, `onKeyDown`, `onKeyUp`, `onTextInput`, `onFocus`, `onBlur`, `onFocusIn`, `onFocusOut` (bubbling) (an `Event -> Void`); `onPress` (`Node.onActivate`: a click, or Enter or Space unless `onKeyDown` took it, as a DOM button's click); the gestures `onTap`, `onPan` (`pan-axis="x"`, `pan-min-pointers`), `onPinch`, `onScale` (a recogniser made on the node, given to its handler: a `Tap`, a `Pan`, a `Scale`). `helder.ui.view.NodeEvents` declares them (NodeRenderer's `@:events`, the neutral elements' in every renderer), and markup checks each handler against it: a function of its event or of none (`onPress={() -> ...}`), an untyped parameter typed as it (`onPan={g -> x(g.tx)}`, `onInput={e -> name(e.text)}`); another type, a Dynamic value or a name not there is a compile error at the attribute (helder.view's README); - `key-context="Viewer zoomed"` (what the node is to the keymap), `actions={[Actions.handle(ViewerAction, a -> ...)]}` (its action handlers), `track-focus={handle}` (a `FocusHandle` tied to it), `tab-index={1}` (Tab's order): see Actions and keymaps below; - `focusable`, `ref={n -> ...}` (the node), `tag="name"` (for `Node.find`); `id` none here (a page's element's, where a page's renderer realises the element). The elements are also the neutral ones of single-source components (`@:neutral`, src/helder/view/README.md): `view:button`, `view:label`... and `text:p`, `text:h2`... are a view and a text here, the element of that name in a page; `` draws lucide's icon of that name (`helder.ui.widgets.Icons`, an inline SVG in a page; `mirrored` flips it right to left, `data-mirrored` in a page); `` and `` are a text field (`TextField`, `TextArea`: native where `Ui.policy` says; a page's ``, `