{"id":51007451,"url":"https://github.com/ewaldhorn/zigdom","last_synced_at":"2026-06-20T22:01:18.465Z","repository":{"id":361657019,"uuid":"1255262402","full_name":"ewaldhorn/zigdom","owner":"ewaldhorn","description":"My Zig DOM access library used for my Zig web and Web Assembly (WASM) projects.","archived":false,"fork":false,"pushed_at":"2026-06-17T11:00:38.000Z","size":523,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T13:05:02.203Z","etag":null,"topics":["dom","dom-manipulation","mit-license","wasm","webassembly","zig","ziglang"],"latest_commit_sha":null,"homepage":"https://ewaldhorn.github.io/zigdom/","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ewaldhorn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-31T16:00:41.000Z","updated_at":"2026-06-17T11:00:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"5ded280c-9738-4b0d-b08f-5351acf71b60","html_url":"https://github.com/ewaldhorn/zigdom","commit_stats":null,"previous_names":["ewaldhorn/zigdom"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ewaldhorn/zigdom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewaldhorn%2Fzigdom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewaldhorn%2Fzigdom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewaldhorn%2Fzigdom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewaldhorn%2Fzigdom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ewaldhorn","download_url":"https://codeload.github.com/ewaldhorn/zigdom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewaldhorn%2Fzigdom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34586666,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","dom-manipulation","mit-license","wasm","webassembly","zig","ziglang"],"created_at":"2026-06-20T22:01:17.412Z","updated_at":"2026-06-20T22:01:18.448Z","avatar_url":"https://github.com/ewaldhorn.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zigdom\n\nA [Zig](https://ziglang.org/) WASM DOM manipulation library.\n\n\u003e [!IMPORTANT]\n\u003e This library is fully compatible with and tested on **Zig 0.16.0**.\n\nThis demonstrates Zig compiled to `wasm32-freestanding` targeting the browser,\nwith a minimal JS glue layer.\n\n## Why\n\nBuilding browser-based dashboards and interactive pages in a systems language is\nsatisfying. Zig gives us tight WASM output, compile-time asset embedding (`@embedFile`),\nand zero hidden runtime overhead.\n\n## What is it for\n\nTo learn Zig, I often build small little games with it. This library makes it simpler and easier\nfor me to do that, as it saves me a lot of setup time for each project.\n\nI've used this to build these:\n- Retro Flyer at https://nofuss.co.za/toys/retro_flyer\n- BlendedFlame at https://nofuss.co.za/toys/zigflame\n- Bouncer at https://nofuss.co.za/toys/bouncer\n\n## Structure\n\n```\nbuild.zig           — Library build definition (module graph for zig fetch)\nbuild.zig.zon       — Package manifest\nsrc/\n  dom.zig           — Core DOM library — low-level JS DOM and Canvas 2D bindings\n  html.zig          — Declarative HTML element builder (chainable, zero-heap)\n  canvas.zig        — In-memory pixel canvas, Bresenham lines, circles, and shapes\n  colour.zig        — RGBA colour structures, grayscale conversions, and PRNG\n  sound.zig         — Zero-heap UI sound effects generator (pre-rendered button click blip)\ndemo/\n  build.zig          — Demo build (depends on parent zigdom library)\n  build.zig.zon      — Demo package manifest\n  src/\n    demo.zig         — Demo entry point (DOM controls, graphics canvases + sound effect export)\n    bodystyle.css    — CSS embedded into the WASM binary at compile time (@embedFile)\n    zigdom.txt       — Text embedded into the WASM binary at compile time (@embedFile)\ndocs/\n  index.html         — Page you open in the browser\n  styles.css         — Page styles\n  zigdom.js          — JS glue — handle table, string bridge, direct-memory canvas, and audio control\n  synth-worklet.js   — Dedicated AudioWorklet processor for the retro soundtrack synth\n  zigdom.wasm        — Built binary (see .gitignore)\n```\n\n## Build \u0026 Run\n\nEnsure you have **Zig 0.16.0** installed, then run:\n\n```bash\n./build.sh          # zig build → docs/zigdom.wasm\n./run.sh            # build + http-server on :9000\n```\n\nOr serve `docs/` with any static server after building.\n\n## Using in Your Project\n\n### Step 1: Add the dependency\n\n```bash\nzig fetch --save git+https://github.com/ewaldhorn/zigdom\n```\n\nThis adds zigdom to your `build.zig.zon`:\n\n```zig\n// in build.zig.zon\n.zigdom = .{\n    .url = \"git+https://github.com/ewaldhorn/zigdom\",\n    .hash = \"...\",   // auto-filled by zig fetch\n},\n```\n\n### Step 2: Wire up your `build.zig`\n\n```zig\nconst std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    const target = b.resolveTargetQuery(.{\n        .cpu_arch = .wasm32,\n        .os_tag = .freestanding,\n    });\n    const optimize = b.standardOptimizeOption(.{});\n\n    // Fetch the zigdom library modules\n    const zigdom_dep = b.dependency(\"zigdom\", .{\n        .target = target,\n        .optimize = optimize,\n    });\n\n    // Create your WASM executable\n    const exe = b.addExecutable(.{\n        .name = \"app\",\n        .root_module = b.createModule(.{\n            .root_source_file = b.path(\"src/main.zig\"),\n            .target = target,\n            .optimize = optimize,\n        }),\n    });\n\n    // Import only the modules you need\n    exe.root_module.addImport(\"dom\", zigdom_dep.module(\"dom\"));\n    exe.root_module.addImport(\"html\", zigdom_dep.module(\"html\"));\n\n    // WASM-specific: no main, export all public symbols\n    exe.entry = .disabled;\n    exe.rdynamic = true;\n\n    b.installArtifact(exe);\n}\n```\n\n\u003e [!TIP]\n\u003e You only need to import the modules your code actually uses — `html`,\n\u003e `canvas`, `colour`, and `sound` are optional. Pick what you need.\n\nSee [`demo/build.zig`](demo/build.zig) for a complete working example\n(including the install-to-`docs/` pattern).\n\n### Step 3: Write your Zig code\n\nImport modules by name — **not** by file path:\n\n```zig\nconst dom = @import(\"dom\");\nconst html = @import(\"html\");\n\nexport fn zig_init() void {\n    dom.init();   // must be called first\n\n    const h1 = dom.createElement(\"h1\");\n    dom.setInnerText(h1, \"Hello from Zig!\");\n    dom.addToBody(h1);\n}\n```\n\n\u003e [!WARNING]\n\u003e `getString` and `Handle.get` return slices that point into a shared 4 KB\n\u003e scratch buffer. **Copy the slice if you need it to persist** across\n\u003e multiple string-retrieval calls.\n\n### Step 4: Add the JS glue\n\nzigdom's Zig modules call into JS functions provided by `zigdom.js`. Copy it\n(and optionally `synth-worklet.js` for the retro soundtrack) into your\nproject:\n\n```bash\ncurl -O https://raw.githubusercontent.com/ewaldhorn/zigdom/main/docs/zigdom.js\ncurl -O https://raw.githubusercontent.com/ewaldhorn/zigdom/main/docs/synth-worklet.js\n```\n\nInclude them in your HTML and load your WASM binary with the built-in\n`ZigDom.instantiate` helper:\n\n```html\n\u003cscript src=\"zigdom.js\"\u003e\u003c/script\u003e\n\u003cscript type=\"module\"\u003e\n  ZigDom.instantiate(\"app.wasm\").catch(err =\u003e {\n    console.error(\"Zigdom failed to load:\", err);\n  });\n\u003c/script\u003e\n```\n\n### Step 5: Build\n\n```bash\nzig build\n```\n\nThe demo builds with `ReleaseSmall` for a compact WASM binary (~24 KB).\nYour WASM binary lands in `zig-out/bin/` by default. If you need it elsewhere\n(like `docs/`), copy `demo/build.zig`'s install step or adapt the install\nstep in your own `build.zig`.\n\n---\n\n### Using a local path (without `zig fetch`)\n\nIf you want to point at a local checkout instead of fetching from GitHub,\nadd a path dependency to your `build.zig.zon`:\n\n```zig\n// in build.zig.zon\n.zigdom = .{\n    .path = \"../zigdom\",\n},\n```\n\nEverything else stays the same — `b.dependency(\"zigdom\", ...)` resolves\nthe local path automatically.\n\n### Quick start code\n\n```zig\nconst dom = @import(\"dom\");\n\nexport fn zig_init() void {\n    dom.init();  // must be called first — captures document/body/head handles\n\n    const h1 = dom.createElement(\"h1\");\n    dom.setInnerText(h1, \"Hello from Zig!\");\n    dom.addToBody(h1);\n}\n```\n\nOr write it declaratively with the `html` builder:\n\n```zig\nconst dom = @import(\"dom\");\nconst html = @import(\"html\");\n\nexport fn zig_init() void {\n    dom.init();\n\n    _ = html.div()\n        .id(\"root\")\n        .child(html.h1().text(\"Hello from Zig!\").build())\n        .child(html.p().text(\"Rendered with zigdom.\").build())\n        .appendTo(dom.body);\n}\n```\n\nThe builder and handle-based API share the same underlying handle table and\ncan be mixed freely.\n\nIf your app responds to DOM events or drives an animation loop, also export\n`zig_invoke_callback`. JS will call it with the numeric ID you registered:\n\n```zig\nexport fn zig_invoke_callback(id: u32) void {\n    switch (id) {\n        0 =\u003e myButtonHandler(),\n        1 =\u003e myAnimationTick(),\n        else =\u003e {},\n    }\n}\n```\n\nIf you use `zig_set_interaction` for canvas touch/click coordinates, export\nthat function too:\n\n```zig\nexport fn zig_set_interaction(x: i32, y: i32) void {\n    // store x, y for the next callback invocation\n}\n```\n\n## How It Works\n\nZigdom uses a lightweight JS bridge:\n\n- **Handle table** — JS stores references to live DOM elements in an array.\n  Zig passes type-safe handles (an `extern struct` wrapping a `u32` ID) instead of raw pointers.\n- **String bridge** — Strings are passed as `(ptr, len)` pairs into WASM\n  linear memory. JS reads/writes via `TextDecoder`/`TextEncoder`.\n- **Callback table** — Zig exports `zig_invoke_callback(u32)`. JS event\n  listeners and `requestAnimationFrame` call it by ID when events fire.\n- **Zero-copy canvas** — The pixel buffer lives inside WASM memory. JS\n  creates a `Uint8ClampedArray` view directly on it and calls `putImageData`\n  — no copy between Zig and the browser canvas.\n- **Low-latency UI audio \u0026 dedicated synth thread** — The 1980s retro\n  soundtrack synthesizer runs inside a dedicated browser `AudioWorklet` thread\n  (`docs/synth-worklet.js`) to guarantee pop-free, stutter-free playback under heavy\n  DOM and Canvas rendering. Meanwhile, short UI sound effects (like the 50ms button\n  click) are pre-rendered directly into a static WASM buffer inside `src/sound.zig`.\n  On first play, JavaScript wraps the WASM memory buffer in a zero-copy `Float32Array`\n  view, copies it to a native AudioBuffer, and triggers it with sub-millisecond,\n  hardware-accelerated latency. Both share a lazily-initialized browser `AudioContext`\n  with decoupled node setup.\n- **No GC** — No hidden allocations, no finalizers. All data lives in\n  fixed-size global arrays in the WASM data segment.\n\n## API\n\n### `dom` — Core DOM\n\n| Concern | Functions |\n|---|---|\n| Initialisation | `init()` |\n| Element creation | `createElement`, `createDiv`, `createParagraph`, `createParagraphWithText`, `createButton`, `createImg` |\n| Element access | `getElementById`, `getString`, `setValue`, `setFocus` |\n| Element manipulation | `addElementTo`, `addToBody`, `removeAllChildElementsFrom`, `wrapElementWithNewDiv` |\n| Inner content | `setInnerText`, `setInnerHTML` |\n| Property access (by handle) | `set`, `get` |\n| Style (by element ID) | `addNewStyleElement`, `addClass`, `removeClass` |\n| Style (by handle) | `addClassTo`, `removeClassFrom`, `replaceClasses` |\n| Visibility | `hide`, `show` |\n| Events | `addEventListener`, `addEventListenerById` |\n| Animation | `startAnimationLoop` |\n| Utilities | `log`, `showAlert` |\n\n### `html` — Declarative HTML Builder\n\n| Concern | Methods / Constructors |\n|---|---|\n| Builder struct | `Elm.handle` (the dom.Handle), `Elm.init(\"tag\")` |\n| Chain methods | `.id(str)`, `.class(str)`, `.text(str)`, `.html(str)`, `.attr(key,val)`, `.child(handle)`, `.appendTo(handle)`, `.on(event, cb_id)`, `.build()` |\n| Structural tags | `div()`, `span()`, `p()`, `button()`, `a()` |\n| Headings | `h1()`–`h6()` |\n| Semantic | `article()`, `aside()`, `section()`, `nav()`, `header()`, `footer()`, `main_tag()` |\n| Lists | `ul()`, `ol()`, `li()`, `dl()`, `dt()`, `dd()` |\n| Inline text | `strong()`, `em()`, `code()`, `pre()`, `small()`, `mark()`, `b()`, `i()` |\n| Form | `form()`, `input()`, `label()`, `select()`, `option()`, `textarea()`, `fieldset()`, `legend()` |\n| Media | `img()`, `br()`, `hr()` |\n| Table | `table()`, `thead()`, `tbody()`, `tr()`, `th()`, `td()` |\n| Misc | `figure()`, `figcaption()`, `details()`, `summary()`, `blockquote()`, `cite()`, `time()` |\n\n\u003e [!NOTE]\n\u003e `.child()` accepts a `dom.Handle` — pass the result of `.build()` from a\n\u003e child sub-tree. `.appendTo()` accepts a parent `dom.Handle` and returns\n\u003e `*const Elm` for further chaining. Use `.on(\"click\", cb_id)` to attach\n\u003e event listeners inline during construction.\n\n### `canvas` — In-Memory Pixel Canvas\n\nAll drawing goes into a WASM-side byte buffer; call `Canvas.render()` to blit\nit to the browser canvas in one zero-copy operation.\n\n| Concern | Functions |\n|---|---|\n| Lifecycle | `Canvas.init`, `Canvas.render` |\n| Fill | `Canvas.clearScreen` |\n| Colour state | `Canvas.setColour`, `Canvas.getColour` |\n| Pixels | `Canvas.putPixel`, `Canvas.colourPutPixel`, `Canvas.getPixel` |\n| Lines | `Canvas.line`, `Canvas.colourLine`, `Canvas.linePoint`, `Canvas.colourLinePoint` |\n| Circles | `Canvas.circle`, `Canvas.colourCircle`, `Canvas.filledCircle`, `Canvas.colourFilledCircle`, `Canvas.borderCircle`, `Canvas.colourBorderCircle` |\n| Rectangles | `Canvas.filledRectangle`, `Canvas.colourFilledRectangle`, `Canvas.rectangle`, `Canvas.colourRectangle` |\n| Triangles | `Canvas.triangle` |\n| 2D context | `Canvas.getContext2D` → `Context2D.beginPath`, `.fill`, `.arc`, `.fillStyle` |\n\n### `colour` — Colour Primitives\n\n| Concern | Functions / Types |\n|---|---|\n| Type | `Colour` — `{ r, g, b, a: u8 }` |\n| Constants | `Colour.white`, `Colour.black`, `Colour.empty` |\n| Queries | `Colour.isEmpty` |\n| Conversions | `Colour.convertToGrayscale` |\n| PRNG | `randomColour()`, `seed(u64)` |\n\n\u003e [!NOTE]\n\u003e `Point` (`{ x, y: i32 }`) is defined in `canvas.zig`, not `colour.zig`.\n\n\u003e [!NOTE]\n`randomColour()` and `Colour.convertToGrayscale` use an internal\nxorshift64 PRNG seeded at 1337. Call `colour.seed(n)` with a non-zero\nvalue to get a different random sequence.\n\n### `sound` — Zero-Heap Sound Effects\n\n| Concern | Functions / Types |\n|---|---|\n| Sound Effects | `fillClick(buf: []f32)` |\n| Configuration | `SAMPLE_RATE` (44.1kHz) |\n\n\u003e [!NOTE]\n\u003e `sound.zig` now only provides the UI click generator (`fillClick`). The retro soundtrack synthesizer was ported to `docs/synth-worklet.js` (browser AudioWorklet) for dedicated audio-thread performance.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fewaldhorn%2Fzigdom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fewaldhorn%2Fzigdom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fewaldhorn%2Fzigdom/lists"}