There are two people I keep meeting, and for a while I was both of them.
The first is a designer. They have a social post that needs to go out for forty products, so they duplicate the artboard forty times and change the name, the price and the photo by hand. Around copy twenty-six they ask the question that every designer eventually asks: is there a way to do this once?
The second is a developer. They need forty product images for the shop, and the only design tool they trust is code. So they write an HTML page, open it in a headless browser, take forty screenshots and spend the afternoon nudging a div three pixels to the left.
The designer has just described a loop. The developer has just rebuilt a design tool out of a browser. They are standing on opposite sides of the same problem, and each is missing exactly what the other already knows.
I have been building bannx to sit in the middle of that gap: a design tool where a layout is something you can see and move with your hands, and a small program with variables, conditions, loops and formulas. This is what I learned building it, and what I think each side should steal from the other.
Every design is already a program
Look at any finished social post, invoice or certificate and you can read the code that a person ran in their head to make it.
- Variables. The parts that change between versions: the name, the price, the photo, the date.
- Conditionals. "If there is no discount this week, drop the badge."
- Loops. "One row per item on the order. One card per speaker."
- Functions. "The panel is a lighter version of the brand colour." "The total is the sum of the rows plus tax."
- Layout as flow. "The button sits under the text, however long the text turns out to be." That is flexbox, drawn instead of typed.
Designers run all of this already. They run it by hand, once per file, and the program only exists in their memory. Developers can write all of it, but in a medium where they cannot see the result until the screenshot comes back.
So the goal was never "teach designers to code" or "teach developers to design". The goal was a single document where both of them are looking at the same program.
Here is what that looks like. These two images are one template. I passed it two different sets of data and changed nothing else.
The checklist is a loop: three services became four rows on their own. The discount badge, the price chip on the photo and the phone number each sit behind a condition, so when that data was empty they left, and the layout closed the gaps. The headline wrapped to more lines and everything under it moved down. Nobody drew the second version.
How I built it: six decisions
1. Variables are inputs, and every input has a default
A template declares its variables the way a function declares parameters: a name, a type and a default. The types are the ones a design actually needs. Plain text, numbers, rich text (so one word can be bold or highlighted), a colour, an image, a list of rows, even a signature drawn on a pad.
The default matters more than the type. A template with no defaults is a function you cannot call without arguments, which in design terms means a blank canvas you cannot preview. Every template in bannx renders on its own with its sample values, so the designer is always looking at a real result, never a wireframe full of {{ placeholders }}.
2. Conditions belong to the layer, not the formula
The first version put visibility logic inside text formulas. It was miserable. You could not tell from looking at the canvas why a layer had vanished.
Now a condition is a property of the layer, like its colour or its position. Select the price chip and you see its rule: isDefined(price). When the rule is false the layer is removed from the layout, not just hidden, and auto-layout closes the space it left. That last part is what makes conditions usable. A hidden layer that still takes up room is a bug the designer has to work around by hand.
3. Loops live in the layer tree, not in the formula language
This was the most important call, and it went against my developer instincts.
The formula language has no for and no while. Instead, any layer can be marked as a repeater and pointed at a list. At render time it is copied once per item, and inside each copy the item has a name, like service, and an index, serviceIndex.
The reason is that a designer needs to see the row. They design one checklist item, at full fidelity, on the canvas, and the data decides how many there are. If loops lived inside formulas, the thing being repeated would be text in a box instead of a shape you can drag.
The same idea scales up. Put a repeat on a whole scene and each item becomes a page. A carousel of eight slides, a deck, a 30-page product catalogue: one designed page, any number of rendered ones. A scene can also auto-paginate when its content is too long, which is how an invoice with three line items and one with sixty come out of the same file.
4. The formula language is small on purpose
The formulas inside {{ }} look like JavaScript. price * 1.2, isDark(color) ? 'white' : 'black', len(headline) > 40. Underneath, they are not JavaScript at all, and that is deliberate.
Templates are shared. They get published to a gallery, opened by strangers and rendered on a server. So a formula is user input, and the engine treats it that way. Each one is parsed into a syntax tree and walked by a small evaluator that only understands a fixed list of operations. Functions come from an allowlist of about a hundred helpers for text, numbers, money, colour, dates and lists. There are no method calls (slice(list, 0, 3), never list.slice(0, 3)), no new, no function, no loops, no way to reach a prototype. A formula is capped in length and complexity, so nobody can write one that takes the server down.
The constraint turned out to help designers as much as security. A small language is a learnable language. Someone who has written a spreadsheet formula can read every expression in a bannx template, and the error messages can be specific because there is so little that can go wrong.
5. Same input, same pixels
A developer calls a function and expects the same answer twice. Designs should behave the same way, and by default they do not: anything "random" reshuffles on every render.
So randomness takes a seed. randomFloat(-3, 3, 'sticker-' + itemIndex) tilts each sticker by its own few degrees, and it is the same few degrees every time. Animation follows the same rule: time is just a variable, $time, so frame 48 of a video is always the same frame.
This is what makes the rest of the system possible. A render can be cached. A signed image URL can be generated before anything is drawn, because the result is a pure function of the template and the data. Fifty people downloading the same certificate do not cost fifty renders.
6. Make the program visible
The risk with all of this is that the design turns into a black box. The canvas shows the result, but not why.
So the program is drawn on top of the design. Type {{ in any field and autocomplete lists what is in scope right now: your variables with their current values, the brand's colours and fonts, the loop item if you are inside one. Any property driven by a formula is tinted in the inspector, and the selected layer shows small { } chips naming what it is bound to. A sample switcher steps the canvas through each set of test data, so a designer can watch the long-name case and the empty-discount case without typing them in.
That is what I mean by a program you can see. The logic is not in a separate script window. It is attached to the shapes it controls.
If you are a designer learning to code
You already think in these structures. What you are learning is to write them down once instead of performing them forty times.
Start with three, in this order. A fallback, so nothing is ever empty: {{ use(logo, iconFallback) }}. A condition, so optional things can leave. A loop, so lists stop being something you rearrange by hand.
Then design the worst case before the nice one. The longest name, the missing photo, the order with forty items. That is the designer's version of writing tests, and it is where a template proves it works.
If you are a developer who has to make graphics
Stop placing things by coordinates. A layout that says "this sits under that, 24 pixels apart" survives new data. A layout that says "this sits at y = 312" breaks the first time a headline wraps. Auto-layout is flexbox; use it the way you would in CSS.
Pull colours, fonts and spacing from tokens instead of hard-coding hex values, for the same reason you would not hard-code a URL.
And let someone with taste own the template. The win is not that you no longer need a designer. It is that the designer makes the thing once, and your code supplies the data:
curl -X POST https://bannx.com/api/v1/templates/TEMPLATE_ID/render \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "format": "png", "variables": { "services": ["Deep clean", "Oven refresh"], "price": "" } }' That is a trimmed version of the request behind the second image above: a list for the loop, an empty price for the condition.
Where the analogy breaks
Programming principles make a design repeatable. They do not make it good.
A loop does not know that nine speakers is too many for one square. A condition does not know that the post looks empty once the badge is gone. A formula can compute a lighter brand colour, but it cannot tell you whether that colour feels right next to the photo. Those are still judgement calls, and they still belong to whoever is designing.
What the structures do is move that judgement to the right moment. You decide once how the design should behave across every version, instead of deciding forty times, tired, at copy twenty-six.
I would like to hear from both sides of this. If you are a designer who started writing code, or a developer who ended up making graphics, which idea crossed over first for you?



