Product & UX · 12 min read

Postmo

A browser tool for making motion posters: live animated fills, type and video on one canvas, exported as a video, an image or a web page.

The editor with Flying Silk open. The colour die fits a new palette to the shader's light and dark, and the pattern die changes the seed. Seed 0 is always the original design.

TL;DR

Problem
Some photographs have an atmosphere that could change a graphic design if the picture moved. Could a browser render an image like that live, with the motion under the designer's control?
Solution
Postmo, a browser editor where live shaders, type, images and video share one canvas. A shader is a fill like any other, and a design exports as a video, an image or a web page.
Result
Live at postmo.dev. I designed it end to end and built it with an AI coding agent, and every change has to pass about 120 browser tests and more than 600 unit tests.
Try Postmo on desktop ↗ (opens in a new tab)
Role
Product designer: product, interaction, visual and design system
Timeline
September to October 2026
Platform
Desktop web (editor) · mobile web (intro site)
Tools
Claude Code, WebGL
Status
Live at postmo.dev
  • 56problems logged when I audited the first version against Figma
  • 3 daysto rebuild shaders as fills, in three steps (Case 1)
  • 5versions of an audio timeline built in one afternoon, then all cut (Case 3)
  • 600+unit tests, plus about 120 browser tests, that every change must pass

As a photographer, I kept wondering what some photographs would look like in a graphic design if they could move. Postmo grew out of that question. It is a browser editor where live shaders, type, images and video share one canvas and export as a video, an image or a self-contained web page. I designed it end to end and shipped it at postmo.dev, with an AI coding agent as the programmer and tester.

Context

I am a photographer as well as a designer, and I look at a lot of photography online. Some photos have a strong atmosphere: light through leaves on a plaster wall, a street reflected in a rain puddle, trees pulled into streaks by a slow shutter from a moving car. With some of them I wondered whether a graphic design would look different if the picture moved.

That led to a practical question: could a browser composite an image like that and render it live, with the motion under the designer’s control? The project started as Motion Poster, and most of its built-in shaders began from a photograph. Each one was built so its default look reads like the photo, and its colours, pattern and speed can all be changed from there.

As the editor grew beyond posters, the name became Postmo. It joins poster and motion in one word, which is why the MO in the wordmark carries a green trail, and it also reads as a short form of postmodern. Postmo is a browser editor for designers who already work in online design tools.

The same five in motion. Each one loops seamlessly every 8 seconds.

Case 1 · A shader became a fill, so every layer is shape × fill

Question
Could a shader cover only part of the canvas, like any other design element?
Options
A crop box on the effect layer; a separate Mask layer for shaders inside shapes; or treat the shader like an image, as a fill inside any shape.
Trade-off
The crop box was too small a change. Separate layer types meant people had to pick the right type before they knew what they wanted.
Decision
Rebuild every layer as a shape plus a fill, the model design tools already use. A shader is one of four fills.

The first idea was simple. A shader would be the background, and text, shapes and images would sit on top of it. Every template was built that way. The shader layer always covered the whole canvas, its panel was one Effect layer section with the shader’s colours and sliders, and the background colour underneath had nothing to show.

Before

The editor before the change, with the shader as a full-canvas effect layer
  1. The shader is an effect layer (FX)
  2. One panel for its colours and parameters

Now

A title filled with a shader
  1. The title is a text layer
  2. A shader fills its letters
  3. The background is a shader layer (SDR)
Before: the shader was an effect layer (FX) that always filled the canvas. Now: the title is a text layer with a Shader fill, still edited as text.

The question that changed it

On September 30, while using the editor, I asked whether the effect layer could be resized and reshaped to cover only part of the canvas. The first answer was a crop box on the effect layer. Within a couple of hours I saw that this was too small a change. A shader could be a design element like any other, something you scale, crop, mask and animate. So I looked at how design tools treat its closest equivalent, the image, and rebuilt the shader around that in three steps over three days.

Step 1: a shader as a generated image

In Figma an image is a fill inside an ordinary frame, and a fill mode decides what happens when the frame changes. I used the same model. A shader layer became a normal layer with a frame that can be moved, resized, rotated, picked by layer order and animated with Loop motion. In Fill mode the shader redraws at the frame’s size, so a smaller frame gives a smaller, recomposed picture rather than a stretched one. I accepted that, because it suits something generated. In Crop mode the shader keeps its own rectangle, which you enter by double-clicking, as with images elsewhere, or from a button in the panel. A shader that fills the canvas looks exactly as before, so no template had to change.

Step 2: a mask layer, kept light

Next I wanted shaders inside text and simple shapes. General-purpose masks need groups and layer ordering, which felt too heavy for this product, so I added one fixed component instead: a Mask layer made of a shape (text, an ellipse or a rectangle) with a shader inside, similar to Canva’s Frames. Over the next day it grew image masks, edge softness, an Outside setting that lets light spill past the shape, and more shapes.

The Mask layer, with a shader showing through the word MOTION
  1. The Mask layer (MSK)
  2. A shader shows through the word
  3. Shape: text, ellipse or rectangle
Step 2: the Mask layer (MSK), a shape with a shader inside. Here the word MOTION shows a shader through its letters.

Step 3: one model, shape × fill

By October 2 the editor had the same idea built three times. Shader layers could be any size but only rectangles. Mask layers could be any shape but could only hold a shader. Shape layers could be any shape but could only hold a solid colour. What a layer contained decided what it could do, so people had to pick the right type before they knew what they wanted. I looked at how online design tools handle this, and rebuilt Postmo on their answer: a layer is a shape plus a fill.

Every layer now answers two questions. Its Shape can be a rectangle, ellipse, polygon, star, arrow, text or a shape taken from an image. Its Fill can be Solid, Gradient, Shader or Media. Switching the fill keeps the frame, shape, effects and motion. The Mask layer type disappeared, and older files convert when they open. Text takes fills too, so a headline can be filled with a shader and still be edited as text. The + menu changed from Effect, Mask and Block to a set of shapes, Text, and two ready-made entries, Shader and Image or video, which are rectangles with that fill.

Fill and Shape are separate sections. Changing the shape keeps the fill, and resizing the frame redraws the shader at the new size instead of stretching it.

Because a shader is now a fill, the templates did not need rewriting, and the next feature became possible: a user’s own shader could be a fill too.

Case 2 · Custom code gets everything a built-in shader has

Options
A separate code layer; ISF's JSON header for parameters; or a custom shader as an ordinary Shader fill, with parameters in a comment on the uniform line.
Trade-off
A code layer would need a second version of every control. ISF keeps a parameter's details away from its code.
Decision
A custom shader is just a Shader fill, so crop, shapes, seed and colour locks all work on it. Each parameter is one commented line, and ISF stays as an import format.

The built-in shaders are mine, and each one started from a photograph. People who write code, or ask an AI to write it, will want their own look, and a lot of good shader code already exists on Shadertoy and in tutorials. I wanted anyone to paste their own GLSL and use it as a fill. I wrote the plan on October 2, shipped the first version on October 3, and reworked the way in and the AI help on October 4.

A custom shader is a Shader fill like any built-in one (Case 1), so the frame, crop, shapes, effects, seed, colour die, colour locks and Reset all work on it without a second version of each. The code is saved in the design file, so copying and the web export carry it, and every layer that shares a shader updates when its code changes.

Parameters live in the code

Custom code needed a way to declare sliders. I looked at ISF, an existing format that declares inputs in a JSON header, and chose something simpler: a comment on the uniform line.

uniform float speed; // @param Speed 0 2 0.01 1
uniform vec3 glow;   // @color Glow #58ff3e

The code stays plain GLSL that other tools can compile, everything about a parameter is on one line, and a new line gives a new slider as soon as it compiles. ISF is still supported, but as an import format. Custom shaders have the same limits as built-in ones, 10 parameters and 8 colours. Colours are declared instead of written into the code, so the colour die and locks work on them too.

A new custom shader with its code window open
  1. Start from code you have: import, paste or drop
  2. The starter shader, with parameters declared in comments
  3. Declared colours, open to the colour die and locks
  4. Declared parameters, shown as sliders
A new custom shader. The parameters and colours declared in the code appear in the panel like a built-in shader's.

Import, errors and loops

Import converts Shadertoy, older WebGL, GLSL Sandbox and ISF code, and a note at the top of the window says what changed, including that Shadertoy code is CC BY-NC-SA unless its author says otherwise. Code Postmo does not recognise goes in unchanged, because a compile error that names the line tells people more than a refusal. Imported code rarely has parameters, so selecting a number and pressing ⌥⌘P turns it into a slider without changing the picture. The first version had six ways in, and on October 4 I merged them: New shader opens the code window with a starter, and importing, dropping and pasting all happen there.

When code fails to compile, the layer keeps drawing the last version that worked and marks the error on the user’s own line. A shader heavy enough to stall the GPU is held back for the session. Looping is a rule for built-in shaders and only information for custom ones: the editor checks one loop of 60 small frames, and the status bar says whether it loops, in neutral text, without blocking an export.

A Shadertoy shader after import, with a note at the top listing what was converted. Select a number, press ⌥⌘P, and it becomes a slider that changes the picture live.

Writing it with AI

Most designers will not write GLSL themselves, but they can ask an AI to. Without guidance, the AI makes the same mistakes every time, and each causes a real problem in Postmo:

What an AI writes by default What happens in Postmo
Numbers written into the maths No sliders in the panel
Time fed straight into noise The loop jumps at the end
Patterns placed with gl_FragCoord The pattern slips when the layer is cropped
Colours written into the code The colour die and locks cannot touch them
Its own noise functions The seed does nothing

Copy AI instructions puts the rules on the clipboard for any AI chat. An open-source kit on GitHub gives tools like Claude Code and Cursor the same rules as a skill, plus a check script that compiles the shader with Postmo’s own renderer and reports errors, looping, seed and frame cost, with PNG frames the AI can look at. When something goes wrong in the editor, Copy for AI copies the instructions, the code and the problems in one go. All three come from one source, so the editor and the kit always agree.

The README of the postmo-shader kit on GitHub
The open-source kit: a skill, the same rules as plain text, examples and a check script.

Case 3 · I cut a timeline I had built five times

Options
Keep growing the audio timeline toward a full editor, or give people only what picking a start point needs.
Trade-off
Each version was a sensible step from the one before, but together they turned Postmo into a video editor.
Decision
Ship Start offset, two fades and Match, and archive the timeline. Apply look was removed for the same reason.

Postmo has fewer features than I built, and three decisions were about removing things.

At first, audio looped like everything else: short files repeated, and each pass crossfaded into the next. Most people bring a song or a voice-over, though, and they would hear it restart every loop. Audio now plays once from the point you choose. This is where looping stopped being a rule for the whole product.

People then needed a way to choose which part of a song plays. On October 3 I went through five versions in one afternoon: Start and End fields, a drawer for sliding the waveform, a small timeline with trim handles and zoom, a full editor timeline with a movable clip, and a simpler track meant to grow into a timeline for every layer. After the fifth, I removed all of it. Each version was a sensible step from the one before, but together they added up to a video editor inside Postmo. People who edit sound already have tools for it, and people who don’t only need to pick where the song starts. What shipped is Start offset, two fades and a Match button. The timeline is archived on a branch.

Apply look moved a template’s shaders onto the current design, and as the editor grew it began replacing the user’s own shader layers too. I removed it: templates now only start a new design.

Four versions of the audio track below the canvas
Versions two to five, top to bottom. All four are archived on a branch.
The shipped Audio section
What shipped: one track row, three sliders and Match.

Case 4 · Phones get a live reel that never plays the same twice

Options
A message asking for a desktop; the full editor on phones; or a separate page that shows what Postmo makes.
Trade-off
The message shows nothing to someone opening a shared link, and the editor needs a desktop.
Decision
Keep the editor off phones and build a reel of live templates whose seeded layouts must pass readability rules, with a video fallback.

The editor needs a desktop, so the original plan gave phones a message saying so. But links get opened on phones, including links shared on LinkedIn, and that message shows nothing. I kept the editor off phones and designed a separate page that shows what the product makes.

The first screen is a reel of two to four live templates, each with a random seed and starting point, so it differs on every visit. A headline and three short sentences are spread across the cuts, and each cut stays on screen as long as it takes to read. Shaders that draw text carry the copy themselves: Neon Echo’s sign spells the headline.

Type size, alignment, line breaks and position all come from the seed, so every layout has to pass rules first: inside the margins, no overlaps, a headline of at least 28 px and body text of 13 to 17 px. The layouts that pass are ranked by contrast, measured on a tiny brightness map of the shader at the start, middle and end of the cut. Body text that still reads poorly gets a small label behind it. Phones that cannot keep up get a recorded video instead.

The phone site on two visits
The phone site on two visits, each with a different template, seed and type layout.
A cut in the reel: the old text fades out, the pictures cross-fade, and the new text fades in.

The system

The canvas in Postmo is full of colour and movement, so the interface around it stays plain, like an instrument printed on paper. I set up the design system before building any of the interface, and every screen was built from it.

Postmo's colour tokens and typefaces
The colour tokens and the three typefaces. The colour values have not changed since the first day.

Panels sit on cool grey paper and the canvas on a neutral stage grey, so the paper’s tint does not change how you judge colour. Text is ink, at 16:1. The one accent, a fluorescent green called flash, is only ever a solid fill where you are meant to act. Kode Mono sets labels and section titles, JetBrains Mono sets numbers that change, and Instrument Sans sets everything else. Corners are square, controls have a 1 px ink border, motion is an 80 ms colour change, and each view has at most one primary button.

I drew the icons to match Kode Mono, on a 24-unit grid at 12 px. The four Fill types were hard to tell apart as icons, so they became words. The tokens live in one file shared by the editor and the phone site, and the written system is part of the spec the AI agent reads every session. It has been revised in 77 commits.

Principles

Five principles guided most of the decisions above.

  1. [01]

    Work like the design tools people know

    I audited the first version, a form, against Figma and logged 56 problems. If designers already know an interaction, Postmo copies it; where design tools have nothing to copy, it follows the closest thing they have (Case 1).

  2. [02]

    Built-in visuals loop

    Every built-in shader loops, and a test checks that each template's last frame leads back into its first. Custom shaders and audio do not have to (Cases 2 and 3).

  3. [03]

    Undo, don't ask

    Postmo has no confirmation dialogs. Destructive actions can be undone and show a short message with an Undo button.

  4. [04]

    Random, within the designer's rules

    The dice for pattern, colour and layout work within limits. Seed 0 and layout 0 are always the original design, and locked colours and layers stay put (Case 4).

  5. [05]

    Your work stays on your computer

    Designs and media are stored in the browser and are not uploaded. Usage statistics record a few action names, with no text, file names, IP addresses or cookies.

How I built it

I am a product designer. On Postmo I was responsible for the design thinking and the final result. An AI coding agent, Claude Code, worked as the programmer and the tester: I decided what to build and judged whether it was right, and the agent wrote and tested the code.

For a new shader, I gave the agent a reference photograph and a short description of what mattered. For interface work, I sent a screenshot of the problem and the behaviour I expected from a design tool. Larger changes went into phased plans that listed my decisions, fourteen in all, and the decisions were collected in a spec the agent reads every session. Some became tests: one fails if a new shader has more than ten parameters.

I reviewed the product by using it, the way I would review a developer’s build. When the word “Saved” crossed the panel line below it, the agent rebuilt the top bar on the panels’ columns. When thin wrinkle lines in Sheer Pressure looked unnatural, I had them removed. A change shipped only after the tests passed: every template has to render, loop and export, alongside about 120 browser tests and more than 600 unit tests.

Outcome

Postmo is live at postmo.dev. It needs no account, and it is fully supported in Chrome and Edge on desktop. On a phone, the same link opens the intro site.

Postmo has only just shipped, so there is no usage data yet. I presented it at Shopify Builder Sundays in Ottawa.

Next, I will look at the first weeks of usage (designs started, exports and custom shaders added) and run a usability test with 3 to 5 designers who use online design tools.

Reflection

The audio redesign, and then undoing it, changed how I think about product decisions. As Case 3 describes, the timeline kept growing because at each step I asked what else a user might want to do with their sound, and then added it.

The problem was in that question. I was trying to give users as much as I could, and that felt like good design while I was doing it. It pulled Postmo toward being a video editor and made me lose sight of what Postmo is: a tool for making a moving design, where sound is something you add near the end.

I now test a new feature against the product’s position before I test it against user needs. If a feature only makes sense in a different product, it is probably the wrong feature, however useful it looks on its own.

What’s next

Layers can loop, but they cannot yet enter, leave or animate with keyframes. That timeline will be designed around layers first, with audio as one track on it. Postmo also needs a list of saved designs, because the browser keeps only one design at a time today.