PROGRESS PATTERNS

The living reference for directive cmp_progress0001 β€” every one of these runs for real. Click them.

🎨 Copy the patterns, not the pixels

The directive requires each app to build progress in its own visual identity. This page is in Appolis's palette on purpose β€” mint and gold on deep navy β€” so it is obvious which parts are Appolis and which are the pattern underneath. A mint bar dropped into an app with a different identity reads as a foreign object, and tells the person something broke.
What travels between apps: when feedback appears, what it says, and how it ends. Not the colours.

β‘  The thin bar β€” a SECONDARY cue, never alone

β›” Rev 5 (Tyler, 2026-09-02): "They should only be creating intuitive progress bars, loading screens, popups, whatever. Should never just be the stupid little flashing bar at the top." Rev 6, an hour later, on seeing a corner card: "never in the bottom right corner. Always a center pop-up as well though. Just saying, waiting isn't gonna cut it either." So the popup sits in the CENTRE of the screen, over a soft scrim that does not block clicks, and it says what the app is doing in specific words β€” never "waiting", never "working". A three-pixel line at the top edge tells a person almost nothing β€” not what is happening, not where, not how long β€” and most people never look there. It may run BESIDE an intuitive indicator as a global "something is in flight" cue. On its own it is not feedback, and an app whose only answer to a wait is this line does not pass criterion 2. Every demo on this page that shows the bar shows it next to the thing that actually answers the question.
Restraint is still the first rule. Criterion 1 (rev 4): "an indicator appears only when there is a genuine wait β€” never for work that is already done… an indicator on work that completes immediately is worse than none, because it tells them they are waiting when they are not."
Watch the centre of the screen β€” and, secondarily, the very top of the window.
⚠️ The third button is the important one, and it is meant to do nothing. A signal that is always on tells you nothing β€” it becomes furniture. The threshold here is 700ms, and that number is not a guess: the Kosmos lane arrived at it after Tyler rejected two attestations for showing too much ("it just randomly keeps popping up with this finished pop-up… if something loads immediately it's still doing that"). A quick action is answered by the control you pressed β€” see β‘’ β€” not by a global indicator. And when a wait IS real, the bar is never what answers it: the card is.

β‘‘ The wait popup β€” "busy with WHAT, for HOW LONG" ← the one Tyler picked Β· the PRIMARY answer since rev 5 Β· CENTRE of the screen since rev 6

Lifted from the lander-builder intake. A bar answers "is it busy?"; this answers the two questions a person actually has. A bold label naming the work, a live seconds counter so a long wait visibly progresses instead of appearing to hang, and a second line saying what is actually happening ("Fetching your apps, Appolis IDs, teams and settings.") or an honest estimate. It sits in the centre of the screen. A card tucked in a corner is a toast, not an answer β€” and "still working…" as the second line tells the person nothing they did not already know. And no double-telling (Tyler, the same hour, to the Hermes lane: "if a screen already indicates that it's loading in some way, it doesn't need an additional pop-up"): a screen that already shows a SPECIFIC labelled loading state where the content will appear β€” "Loading the at-risk queue…" where the queue will be, a full-screen boot screen β€” has answered the question and needs no popup on top of it.
⚠️ The hint is an honest estimate or it is worse than nothing. The lander builder once said "60-120 seconds" against a budget four times smaller, so the screen told the owner a run had failed while it was working perfectly β€” and the live timer beside it visibly disagreed. If the work gets slower, the string moves too.
πŸ” Appolis itself uses this on every wait now β€” v0.23.0. Until rev 5 this note said the opposite ("nothing in this portal takes longer than about a second"), and it was true of the SAVES: they answer on the button (β‘’). It was not true of the LOADS β€” the city loading, a panel reloading after an action β€” which showed nothing but the line at the top. So the portal's shared helper now shows a centred wait popup for any wait over 700ms, named by route ("Loading the city…", "Saving email settings…") with a second line saying what is happening and a live seconds counter, over a soft scrim that never blocks a click; the bar runs beside it. (v0.23.0 put it in the bottom-right corner. Tyler's verdict took one look: "never in the bottom right corner." v0.23.1 moved it.) The card is built once and driven from the same reference count as the bar, so it can never outlive the work. Say on your page which waits show which indicator, and be honest about any you did not build and why.

β‘ b Background and polled work β€” invisible by construction

The other half of criterion 1, and the half that actually bit: "nobody asked it to run, so nobody is interrupted by it finishing." A poller on a timer, a prefetch after paint, a quiet refresh β€” none of it was requested, so none of it may take over the screen. In Kosmos this was a real defect: a 45-second board poller kept opening the work window and demanding a click to dismiss a result nobody had asked for.
It must be a property of the helper, not a habit. Appolis's api() takes { bg: true }; Kosmos's does the same. Relying on background calls happening to use a different code path is an accident waiting to be undone by the next person who reaches for the normal helper.
Both take exactly the same time. Only one of them is allowed to interrupt you.
⚠️ Silent means no PROGRESS UI β€” it does not mean no errors. A background call that FAILS may still be something the person needs to know, and swallowing that is the degrade-quietly shape this house keeps meeting. Hide the spinner, never the problem.

β‘’ The busy control β€” "did my click land?"

A different question from "is the app busy", and it needs its own answer. The control that started the work goes disabled and says so itself, so a second click cannot fire β€” which is how duplicate records get created.
Try double-clicking either one.
⚠️ Restore the label on BOTH paths. A control left spinning after a failure is the same lie as no indicator at all, just slower to notice. The second button proves the failure path puts it back.

β‘£ Determinate β€” only when the total is genuinely known

Uploads, N-of-M jobs, multi-step pipelines. The step currently running is named, so a 40-second wait reads as progress rather than as a hang.
⚠️ Never a fake percentage. A bar that animates to 100% on a timer while the real work is still going is worse than no bar: it teaches people that the indicator is decoration, and then they ignore a real one.

β‘€ Failure β€” the indicator is REPLACED by the reason

The spinner must never outlive its work. When something fails, the thing that was spinning becomes the explanation, in the same place the person was already looking.
This is the criterion most often skipped, because the happy path gets built first and the failure path inherits whatever was left behind.

⑦ The outcome β€” success is shown too ← criterion 12 Β· rev 7 Β· 2026-09-04

Tyler, 2026-09-04: "when something completes, whether it's successful or not, you should see an indicator of that as well." His example, the same day, submitting a note in Kosmos: "It loaded with the progress bar, then everything went away and the project board was shown with no indication that the note was successfully saved or did not save." A wait that ends in silence is only half-answered. So the popup that named the work resolves into the result in the same place β€” "βœ“ App saved Β· 3s Β· Done." β€” and only then goes. Failure was already covered (β‘€); this is the success half, which every app had skipped because a green screen looks finished. Where the screen itself visibly shows the result β€” the new row appearing, marked β€” that landing is the outcome and no popup is needed on top (no double-telling still applies).
In the portal: api() passes the outcome into progressEnd(result) from a by-route table (DONE_LABELS) beside the wait labels, so a route added later gets one by construction; a quick action that never showed the popup confirms on its button (β‘’) and pops nothing. Silent disappearance is the failure this criterion names β€” even when the save worked β€” because the person cannot tell success from a silent loss, and the next thing they do is click again.

β‘₯ Everything at once

What a real screen looks like when a person clicks one button: the control goes busy (did my click land?), the centred wait popup names what is happening and for how long (what is it doing?), and the bar runs at the top as a secondary cue (is anything in flight?) β€” three answers to three different questions, not three copies of one. Take the card away and this screen would fail rev 5; take the bar away and it would still pass.

πŸ“„ Now build your own β€” that page IS your proof

Criterion 10, added at Tyler's instruction: "each app that has to make these should have to produce something that shows what they will look like as well, just like this. That'll be the proof."
He is right, and it changes what attesting means. An attestation is words β€” a convincing paragraph can be written about indicators that were half-built, and the only way to catch that is to go and read the code. A clickable page cannot be faked without actually building the thing. It also turns review into a 30-second job, and leaves whoever maintains your app next something to check against.
What yours needs
  • Live at /progress.html on your own app β€” the same path everywhere, so the wall is checkable at a glance instead of by hunting five different URLs.
  • Every pattern you actually implemented, each running a genuinely slow fake operation a person can click and watch. Not screenshots. Not a description.
  • In your app's own style β€” its palette, its motion, its weight. Not this one's.
  • Every kind of wait your app has is answered in the CENTRE of the screen, in specific words β€” the standard is a centred popup; a screen that already shows a SPECIFIC labelled loading state where the content will appear needs no second popup on top. Plus, where they fit, a meaningful progress bar or a labelled busy control. Never a corner toast; never just "waiting". A page whose only live demo is a top-edge bar, or a corner card, does not prove criterion 2 (rev 6).
  • Every wait ends with a visible outcome, success or failure, where the person was looking β€” the popup resolves into the result ("βœ“ Saved Β· Done.") or the result visibly lands on the screen; a silent disappearance fails even when the work succeeded (criterion 12, rev 7).
  • A plain statement of which waits show which indicator, and which patterns you did not build and why β€” see the amber note under β‘‘ for how Appolis does it.
  • The URL linked in your attestation.
⚠️ Do not invent a slow job to look complete. If nothing in your app has a knowable total, you have no determinate bar and that is a correct answer. A page claiming a pattern the app does not really use is worse than an honest short one β€” it is the exact thing this directive exists to stop, just dressed up nicely.
Attest with appolis_compliance_attest once it is genuinely live: the version, what changed, how you verified it, and this page's URL as the evidence. The owner can reject a thin attestation, which puts your app straight back to outstanding.
Appolis Β· reference implementation for cmp_progress0001 rev 7 Β· back to the city