Designs
A design is a UI drawn as code: a .design file an agent writes, which
devpage renders on a canvas at high fidelity and lets a reader click through
as a prototype. Designs exist so that an agent can show a screen or a flow
before anyone builds it, and so that people can agree on it fast — with the
agent, or with each other on a deployed devpage. It is
a concept and task article under the viewer; how a design is
parsed, laid out, drawn and streamed is in the
design engine.
flowchart LR
agent["Agent"] -->|"writes"| file["checkout.design"]
file -->|"imports"| button["button.design"]
file --> engine["Design engine<br/>parse · resolve · lay out · paint"]
button --> engine
engine -->|"changed nodes, over gRPC-Web"| canvas["Canvas in the browser<br/>click through the prototype"]
engine -->|"GET checkout.design.png"| png["PNG<br/>the agent looks at its work"]
click engine href "../design-engine/README.md" "The design engine"
Humans never edit a design. They look at it, comment on the page that embeds it, and the agent changes the file. A design is not documentation of the current UI and nobody keeps it in sync with the code; it is a prototype, and the code is the design of what is built. What a design must be is valid: every design it uses exists, every value parses, and devpage reports anything else as a problem.
A first design
The example's button, docs/designs/checkout/button.design, whole:
// A button: one line of text on a filled, rounded box.
A screen using it, cut down from the example's
docs/designs/checkout/checkout.design to its bones:
import Button from "./button.design"
Here is the whole checkout screen, embedded in this page:
Open either file in devpage — devpage docs/designs/checkout/checkout.design, or
link to it from any page — and it is drawn on a canvas. Click Pay $42 and
the prototype moves to confirmation.design.
One design per file
Every design is one .design file, and a design is made of other designs.
There is no separate kind of "component": a button is a design that other
designs use. A file:
- starts with any number of imports, one per line —
import Button from "./button.design"— each a name and a path relative to the file. The path must name a.designfile that exists in the served folder, whether the design uses the import or not. Each import takes a name of its own, and none takes a built-in element's name —Frame,Text,Rect,Ellipse,VectororImage. - holds exactly one root element, which is a
Frame. Itswidthandheightare the design's size; a root sized"hug"takes its content's size. A root takes noxory: it has no place of its own, and an instance goes where the instance's ownxandysay.
Elements, arrays and objects nest at most 256 deep, counting through the designs a design uses — far deeper than any real design. A number or a string inside them is not a level of its own.
Using an imported design places an instance of it: <Button label="Pay" />
draws button.design with label set to "Pay". An instance takes the
attributes every element takes — name, x, y, width,
height, opacity and onClick, each applied to the used design's root —
and the design's props; nothing else. Its look — fill, stroke, corners — is
the design's own, changed only through its props. A design that uses itself, directly or through
others, is a problem.
// starts a comment that runs to the end of the line, outside elements and
between attributes.
Props
A design declares the values it takes in its root's props attribute, each
with a default: props={{ label: "Button", fill: "#2563eb" }}. Only the root
declares props; props on any other element is a problem. Inside the
design, {label} puts a prop's value where it stands — as a whole attribute
value (fill={fill}) or inside a text (<Text>Hi, {name}</Text>). An instance
that leaves a prop out gets its default. Naming a prop the design doesn't
declare is a problem. There are no expressions: a value is a literal or one
prop.
Elements and attributes
Six elements draw — Frame, Text, Rect, Ellipse, Vector and Image —
and an imported name places an instance. Their node types and attributes
follow Figma's document model, written as JSX attributes rather than Figma's
JSON; every one, with its values and defaults, is on
Elements and attributes.
Prototypes
A prototype is designs linked by interactions. onClick on any element
takes one:
| Interaction | What a click does |
|---|---|
"navigate <path>" |
shows the design at path in place of this one |
"back" |
returns to the design shown before |
"overlay <path>" |
shows the design at path centered over this one, which it dims |
"close" |
closes the overlay this element is in |
A path is relative to the file the onClick is written in, like every path in
a design, so an interaction inside an imported design works wherever that
design is used. A path must name a .design file that exists in the served
folder; anything else is a problem.
The element under the pointer that has an interaction takes the click, and a pointer over one turns into a hand. A navigation changes the address, so the browser's back button walks back through the prototype, and every step can be linked to. Each state of a prototype is a design of its own.
Viewing a design
A design is a document: its address shows it on a canvas, centred, at its own size — scaled down to fit the window when it is bigger, never enlarged — and a reader can zoom with the pinch gesture or Ctrl/⌘ and the wheel, and pan by dragging or scrolling. The canvas page has the light/dark switch in its corner, the same switch every page has, and remembers the choice with them.
A design's drawing is its root's box plus anything painted outside it — a shadow, a child placed past an edge — as in Figma's export. Every surface shows the whole drawing, so nothing visible on one is missing from another; a design whose paint stays inside its root draws at exactly its root's size.
A page embeds one with the image syntax —  —
or, in raw HTML, with an image of it — <img src="checkout.design">, written
however HTML allows. An embedded design is shown the same way: at its own
size, never enlarged, scaled down only as far as it needs to fit the page's
width and the window's height — on a slide, the room the slide leaves it —
centred on a quiet backdrop that sets it apart from the text. It is clickable like the design's own address, and a link in the
backdrop's corner opens it on its own canvas page. Embedded designs follow the
page's live updates and its light/dark switch: a design's own colours never
change, but the page behind any part it leaves transparent does. In a PDF there is no window, so an embedded design prints
at its own size, scaled down only to fit the paper's width — or, on a slide,
the room the slide leaves it, as on screen.
While a design is open, saving any file it is made of redraws it in place —
only the parts that changed are sent to the browser. A design deleted while
open shows its no such file problem, and comes back when it is written
again — after a branch switch, say — without a reload.
Looking at a design as a PNG
GET <path>.design.png answers with the design's whole drawing as a PNG,
at 1×, from the same layout and the same scene as the canvas
(one renderer):
This is how an agent looks at its work: write the file, fetch the PNG, look,
fix. devpage keeps every design it has read resolved in memory and paints the
PNG itself on the CPU, without a browser, so the answer arrives as fast as a
page. A page can link to or show the PNG too —
 — and devpage check treats that link as
the design itself.
| Answer | When |
|---|---|
200, image/png |
the design is valid: its whole drawing at 1× |
422, the problems as text/plain, one per line |
the design has problems, or its drawing is bigger than 16,384 pixels a side, which is too big to paint |
404, <address> is not part of the served folder |
<path>.design isn't a file in the served folder, as for any other path |
A real file named checkout.design.png beside the design wins: it is served
as it is, and a link to it is checked as that file.
Problems
A design's problems are shown on the canvas page and on any page that embeds
it, reported by devpage check like a broken link
(Checking links), and returned instead of a PNG:
docs/designs/checkout/checkout.design:7:11: Button has no prop "lable"
docs/designs/checkout/button.design:2:21: padding: expected pixels or an array of 2 or 4, found "12px"
docs/designs/checkout/cart.design:1:1: import ./row.design — no such file
While a design has problems the canvas keeps showing the last valid drawing, under the list of problems.
The rule that the canvas and the PNG are drawn by the same code is A design has one renderer.
Topics
- Elements and attributes — every element, attribute, value and default a design can use.