Examples · Storefront
An online store with reviews and a cart: 77 lines of AppSPEC
A storefront with category filters and sorting, a product page with a picture viewer, star ratings and reviews per product, related products, and a cart with quantities and a subtotal.
Running live in the sandbox with fixed demo data. Anything you add is saved in this browser; nothing leaves your device.
The state graph
4 screens, 3 wires
Screens are nodes and WIRE labels are edges. Screens in blue appear in the navigation; orange edges write a record on the way.
How it works
Walking through the spec
01Screens are states
4 screens, each a <STATE>. shop, cart appear in the navigation because they carry a NAV label. Children lay themselves out by containment: ROW, COL and GRID arrange them, and everything else stacks.
<STATE ID="shop" NAV ICON="store">
…
<STATE ID="product">
…
<STATE ID="cart" NAV ICON="shopping-cart" WIDTH="860px">
…
<STATE ID="done" ALIGN="center">02Screens connect by label
Each <WIRE> names an action and where it leads; a button, card or row that fires the label follows it, taking the clicked record along. This app writes 3 wires out and uses the short forms OPEN, SAVE: OPEN="state" opens an item in that state, and FORM SAVE or UPDATE saves the form to a collection, with the engine adding the wire.
SUBTITLE="{price|money}" META="{category} · ★ {rating}" OPEN="product" />
…
<COL SPAN="2" MIN="260px"><RELATED BY="category,tags" HEADING="You may also like" META="{price|money}" LIMIT="4" OPEN="product" /></COL>
…
<WIRE LABEL="add" SAVE="cart" SET="product={products.id}; name={products.name}; price={products.price}; image={products.thumb}" TOAST="Added to your cart" TO="cart" />
<WIRE LABEL="remove" DELETE="cart" TOAST="Removed from your cart" />
<WIRE LABEL="checkout" CLEAR="cart" TO="done" />03Data flows through the key-value pipe
Every input publishes its value into the pipe under its ID, and every screen can read the pipe with {placeholders}. Writes are declared on wires: add save cart, remove delete cart, checkout clear cart. SET adds key-value pairs to the record being written.
<CATEGORY-NAV ID="cat" SRC="products" FIELD="category" />
<SELECT ID="order" OPTIONS="name:Name,price:Price: low to high,price desc:Price: high to low,rating desc:Top rated" VALUE="name" WIDTH="220px" />
…
<TEXT STYLE="eyebrow" TEXT="{products.category}" />
<TEXT STYLE="h1" TEXT="{products.name}" />
…
<TEXT STYLE="h2" TEXT="{products.price|money}" />
<TEXT STYLE="lead" TEXT="{products.tagline}" />
<TEXT STYLE="muted" TEXT="{products.description}" />
…
<SELECT ID="color" LABEL="Colour" OPTIONS="{products.colors}" FLEX="2" />
<INPUT ID="qty" TYPE="number" LABEL="Quantity" VALUE="1" MIN="1" MAX="9" FLEX="1" />
…
<WIRE LABEL="add" SAVE="cart" SET="product={products.id}; name={products.name}; price={products.price}; image={products.thumb}" TOAST="Added to your cart" TO="cart" />04Social components attach themselves
COMMENT-THREAD, LIKES, RATING, RELATED bind to the record on screen through its GUID (for example products/…). The spec never says which record: the engine supplies it, so the same tags work unchanged on a video, a product or a story.
<RATING />
…
<LIKES />
…
<COL SPAN="3" MIN="300px"><COMMENT-THREAD TITLE="Reviews" RATING="true" PLACEHOLDER="What did you think?" /></COL>
<COL SPAN="2" MIN="260px"><RELATED BY="category,tags" HEADING="You may also like" META="{price|money}" LIMIT="4" OPEN="product" /></COL>05One attribute sets the look
THEME="airbnb" sets every colour, radius and typeface. Change the word and the whole app restyles; all 31 themes work with every component.
<APP ID="store" NAME="Hearth & Field" THEME="airbnb" LOGO="shopping-bag" WIDTH="1200px">Convention over configuration
What the engine infers
Every attribute this spec leaves out has a default inferred from names, data and context. These are the ones this app relies on; writing any of them out would override it.
STATE NAV→Shop,CartNAV with no label uses the state ID, humanisedCARDS ACTION→productOPEN="product" opens the clicked item in that stateCARDS TITLE→nameThe records have a name fieldCARDS IMAGE→imagesThe records have a images fieldRELATED ACTION→productOPEN="product" opens the clicked item in that stateCOL LABEL→Qty,PriceFrom FIELD
What this would typically take
The pieces a conventional stack needs
A typical implementation of the same app assembles these parts, each with its own code, libraries and configuration. The size depends on the team and the tools, so we do not quote a number for it.
Here the same app is one 77-line document built from general-purpose components: BUTTON, CARDS, CATEGORY-NAV, COMMENT-THREAD, DIVIDER, HERO, ICON, INPUT, LIKES, PICTURE, RATING, RELATED, SELECT, TABLE, TEXT, TOTAL.
| Routing | Routes for the catalogue, product pages, the cart and the confirmation page. |
|---|---|
| Product gallery | An image viewer with thumbnails bound to the product being viewed. |
| Reviews and ratings | A reviews model and API per product, a star input and an average. |
| Cart | Cart state, add and remove operations, quantities and a running subtotal. |
| Catalogue | Filtering by category, sorting and a responsive product grid. |
| Recommendations | A related-products query. |
| State and styling | Shared state across screens, components and styles. |
| Build tooling | A bundler, a dev server and a build configuration. |
The full specstore.appspec · 77 lines
<APP ID="store" NAME="Hearth & Field" THEME="airbnb" LOGO="shopping-bag" WIDTH="1200px">
<STATE ID="shop" NAV ICON="store">
<HERO EYEBROW="New season" TITLE="Well-made things for everyday use" SUBTITLE="Eight pieces we would buy again. Free returns within 30 days." />
<ROW JUSTIFY="between">
<CATEGORY-NAV ID="cat" SRC="products" FIELD="category" />
<SELECT ID="order" OPTIONS="name:Name,price:Price: low to high,price desc:Price: high to low,rating desc:Top rated" VALUE="name" WIDTH="220px" />
</ROW>
<CARDS SRC="products" WHERE="category={cat}" SORT="{order}" COLS="4" MIN="200px" RATIO="1/1"
SUBTITLE="{price|money}" META="{category} · ★ {rating}" OPEN="product" />
</STATE>
<STATE ID="product">
<BUTTON STYLE="ghost" SIZE="sm" ICON="arrow-left" LABEL="Back to shop" ACTION="back" />
<ROW ALIGN="start" GAP="48px">
<COL SPAN="3" MIN="300px">
<PICTURE BIND="activeRecord.images" MODE="small" HEIGHT="480px" />
</COL>
<COL SPAN="2" MIN="280px" GAP="14px">
<TEXT STYLE="eyebrow" TEXT="{products.category}" />
<TEXT STYLE="h1" TEXT="{products.name}" />
<RATING />
<TEXT STYLE="h2" TEXT="{products.price|money}" />
<TEXT STYLE="lead" TEXT="{products.tagline}" />
<TEXT STYLE="muted" TEXT="{products.description}" />
<ROW>
<SELECT ID="color" LABEL="Colour" OPTIONS="{products.colors}" FLEX="2" />
<INPUT ID="qty" TYPE="number" LABEL="Quantity" VALUE="1" MIN="1" MAX="9" FLEX="1" />
</ROW>
<ROW>
<BUTTON LABEL="Add to cart" ICON="shopping-bag" SIZE="lg" ACTION="add" FLEX="1" />
<LIKES />
</ROW>
<TEXT STYLE="small" TEXT="Demo store: the cart lives in your browser and nothing is charged." />
</COL>
</ROW>
<DIVIDER />
<ROW ALIGN="start" GAP="48px">
<COL SPAN="3" MIN="300px"><COMMENT-THREAD TITLE="Reviews" RATING="true" PLACEHOLDER="What did you think?" /></COL>
<COL SPAN="2" MIN="260px"><RELATED BY="category,tags" HEADING="You may also like" META="{price|money}" LIMIT="4" OPEN="product" /></COL>
</ROW>
</STATE>
<STATE ID="cart" NAV ICON="shopping-cart" WIDTH="860px">
<ROW JUSTIFY="between">
<TEXT STYLE="h1" TEXT="Your cart" />
<TOTAL SRC="cart" QTY="qty" OP="count" STYLE="badge" />
</ROW>
<TABLE SRC="cart" ACTION="remove" EMPTY="Your cart is empty.">
<COL FIELD="image" LABEL="" TYPE="image" WIDTH="64px" />
<COL FIELD="name" LABEL="Item" />
<COL FIELD="color" LABEL="Colour" />
<COL FIELD="qty" />
<COL FIELD="price" />
</TABLE>
<TEXT STYLE="small" TEXT="Click a row to remove it." />
<ROW JUSTIFY="between">
<TOTAL SRC="cart" FIELD="price" QTY="qty" FORMAT="money" LABEL="Subtotal" STYLE="stat" />
<ROW GAP="8px">
<BUTTON STYLE="secondary" LABEL="Keep shopping" ACTION="shop" />
<BUTTON LABEL="Check out" ICON="lock" ACTION="checkout" />
</ROW>
</ROW>
</STATE>
<STATE ID="done" ALIGN="center">
<ICON NAME="circle-check" SIZE="56" COLOR="var(--success)" />
<TEXT STYLE="h1" TEXT="Order placed (in demo land)" />
<TEXT STYLE="muted" TEXT="No payment was taken and nothing was sent. Your cart has been emptied." />
<BUTTON LABEL="Back to the shop" ACTION="shop" />
</STATE>
<WIRE LABEL="add" SAVE="cart" SET="product={products.id}; name={products.name}; price={products.price}; image={products.thumb}" TOAST="Added to your cart" TO="cart" />
<WIRE LABEL="remove" DELETE="cart" TOAST="Removed from your cart" />
<WIRE LABEL="checkout" CLEAR="cart" TO="done" />
</APP><APP ID="store" NAME="Hearth & Field" THEME="airbnb" LOGO="shopping-bag" WIDTH="1200px">
<STATE ID="shop" NAV="Shop" ICON="store">
<HERO EYEBROW="New season" TITLE="Well-made things for everyday use" SUBTITLE="Eight pieces we would buy again. Free returns within 30 days." />
<ROW JUSTIFY="between">
<CATEGORY-NAV ID="cat" SRC="products" FIELD="category" />
<SELECT ID="order" OPTIONS="name:Name,price:Price: low to high,price desc:Price: high to low,rating desc:Top rated" VALUE="name" WIDTH="220px" />
</ROW>
<CARDS SRC="products" WHERE="category={cat}" SORT="{order}" COLS="4" MIN="200px" RATIO="1/1" SUBTITLE="{price|money}" META="{category} · ★ {rating}" ACTION="product" TITLE="name" IMAGE="images" />
</STATE>
<STATE ID="product">
<BUTTON STYLE="ghost" SIZE="sm" ICON="arrow-left" LABEL="Back to shop" ACTION="back" />
<ROW ALIGN="start" GAP="48px">
<COL SPAN="3" MIN="300px">
<PICTURE BIND="activeRecord.images" MODE="small" HEIGHT="480px" />
</COL>
<COL SPAN="2" MIN="280px" GAP="14px">
<TEXT STYLE="eyebrow" TEXT="{products.category}" />
<TEXT STYLE="h1" TEXT="{products.name}" />
<RATING />
<TEXT STYLE="h2" TEXT="{products.price|money}" />
<TEXT STYLE="lead" TEXT="{products.tagline}" />
<TEXT STYLE="muted" TEXT="{products.description}" />
<ROW>
<SELECT ID="color" LABEL="Colour" OPTIONS="{products.colors}" FLEX="2" />
<INPUT ID="qty" TYPE="number" LABEL="Quantity" VALUE="1" MIN="1" MAX="9" FLEX="1" />
</ROW>
<ROW>
<BUTTON LABEL="Add to cart" ICON="shopping-bag" SIZE="lg" ACTION="add" FLEX="1" />
<LIKES />
</ROW>
<TEXT STYLE="small" TEXT="Demo store: the cart lives in your browser and nothing is charged." />
</COL>
</ROW>
<DIVIDER />
<ROW ALIGN="start" GAP="48px">
<COL SPAN="3" MIN="300px">
<COMMENT-THREAD TITLE="Reviews" RATING="true" PLACEHOLDER="What did you think?" />
</COL>
<COL SPAN="2" MIN="260px">
<RELATED BY="category,tags" HEADING="You may also like" META="{price|money}" LIMIT="4" ACTION="product" />
</COL>
</ROW>
</STATE>
<STATE ID="cart" NAV="Cart" ICON="shopping-cart" WIDTH="860px">
<ROW JUSTIFY="between">
<TEXT STYLE="h1" TEXT="Your cart" />
<TOTAL SRC="cart" QTY="qty" OP="count" STYLE="badge" />
</ROW>
<TABLE SRC="cart" ACTION="remove" EMPTY="Your cart is empty.">
<COL FIELD="image" LABEL="" TYPE="image" WIDTH="64px" />
<COL FIELD="name" LABEL="Item" />
<COL FIELD="color" LABEL="Colour" />
<COL FIELD="qty" LABEL="Qty" />
<COL FIELD="price" LABEL="Price" />
</TABLE>
<TEXT STYLE="small" TEXT="Click a row to remove it." />
<ROW JUSTIFY="between">
<TOTAL SRC="cart" FIELD="price" QTY="qty" FORMAT="money" LABEL="Subtotal" STYLE="stat" />
<ROW GAP="8px">
<BUTTON STYLE="secondary" LABEL="Keep shopping" ACTION="shop" />
<BUTTON LABEL="Check out" ICON="lock" ACTION="checkout" />
</ROW>
</ROW>
</STATE>
<STATE ID="done" ALIGN="center">
<ICON NAME="circle-check" SIZE="56" COLOR="var(--success)" />
<TEXT STYLE="h1" TEXT="Order placed (in demo land)" />
<TEXT STYLE="muted" TEXT="No payment was taken and nothing was sent. Your cart has been emptied." />
<BUTTON LABEL="Back to the shop" ACTION="shop" />
</STATE>
<WIRE LABEL="add" SAVE="cart" SET="product={products.id}; name={products.name}; price={products.price}; image={products.thumb}" TOAST="Added to your cart" TO="cart" />
<WIRE LABEL="remove" DELETE="cart" TOAST="Removed from your cart" />
<WIRE LABEL="checkout" CLEAR="cart" TO="done" />
</APP>