SparkStartGuideReferencePlatformsCompare GitHub

Guide

The whole model, in the order you meet it.

Structure is layout

<row gap="8" align="center">
  <image src="icon.svg" w="20" h="20">
  <text grow>A row lines things up</text>
  <button>Go</button>
</row>

<row> lines up, <col> stacks, <grid cols="3"> tiles, <stack> layers, <scroll> clips. A box measures itself from its content and its padding unless you give it w or h. Something with grow takes its share of what is left over — its share of the free space, not its content plus a share, which is what makes two of them exactly equal.

Attributes are the cosmetics

Written where the thing is, because there is no selector to write them somewhere else. Any of them can be set from a script by the same name, and read back as what it is:

card.w        // 120, a number — not "120"
card.w + 10   // 130
card.bold     // true, a boolean
card.bg       // "#222222", a string

Named styles, without a cascade

<style name="card" bg="#222" radius="10" border="1" border-color="#333" pad="16">

<col use="card"> … </col>

A bundle of attributes under a name. It does not cascade, it does not match, and it cannot reach anything it was not written on.

Templates: markup by name

<template name="kv">
  <row gap="8"><text grow>$k</text><text>$v</text></row>
</template>

<kv k="Version" v="1.0.0">

Text in, text out, while parsing. Nothing is left at runtime, and a template cannot see anything but what its call hands it — which is what stops it becoming a scope.

Building what is not in the box

Four layers, in the order to reach for them:

a look<style> + use=""
markup<template>
a drawing<surface> with <fill> and <picture>
behavioura function over ids
<surface h="60" self="stretch">
  <fill id="knob" at="20 10 40 40" radius="20" bg="#4a90e2">
</surface>

A drawn shape is a real target: give that <fill> a click listener and it is hit-tested, hovered and given a hand cursor like any widget. A knob, a colour wheel, a chart with tooltips, a confetti burst — all of it without adding anything to Spark.

Movement

<picture src="sword.svg" animate="rotate 2s alternate" from="-5" to="5">
<col use="card" animate="scale 150ms" from="0.86" to="1">

One numeric attribute between two values. A one-shot animation runs again every time its node is shown, which is what makes a dialog pop open rather than pop open once. The clock runs at sixty a second only while something is moving, and honours the system's reduced-motion setting without being asked. For anything Spark has no opinion about, listen for frame.

Events

button.addEventListener("click", save);
document.body.addEventListener("key", function () {
  if (document.body.key === "Escape") close();
});
card.addEventListener("drop", function () {
  open(String(document.body.files).split("\n")[0]);
});

Spark has no event object — a listener is a function of no arguments everywhere — so what came with an event is read off <body> like any other property. Key names are decided once for all three systems, so nobody has to know whether this one calls it Return or Enter.

More than one window

spark.open("settings.html");   // a second window, a second document
spark.tell("saved");           // a line to every other window

A window is a document: its own tree, its own script context, its own clock. They share a process and nothing else, so the only thing one can do to another is tell it something.

What the system already runs

spark.file.write(spark.dir("config") + "/settings.json", text);
spark.notify("Build finished", name);
spark.tray("", "Running");
spark.hotkey("ctrl+alt+G");
spark.sound("sounds/done.wav", 0.5);
spark.openFile();  spark.run("git status");  spark.writeClipboard(text);

Dialogs, the clipboard, notifications, the status area, global shortcuts, standard folders, running a program, opening a URL. Every one of them is the system's own, and every one answers null where the system has no such thing — so an app can ask and carry on.

npm packages, without shipping a runtime

<script src="main.js" bundle></script>

The developer's own esbuild and Babel translate the script down to es5 before it is baked in — never on the machine that runs the app. They have Node because they installed the packages; the person using the app needs neither.

Measured, on ten pieces of modern syntax: JavaScriptCore on macOS and Linux accepts all ten. The JScript that ships with Wine accepts JSON and nothing else — no let, no arrow, no class, no Promise. That is why the target is es5 and why this exists.