Saving footer
A form that saves itself when the person pauses typing, with a footer that says Saving, Saved at 14:32, or why it could not save. Offline, it shows the connection with fireflies and saves by itself once the connection is back. Driven by a pure, seeded fake transport you swap for your own.
quiet warm playful
Open the live demo
npx susegad add saving-footer
Stands on: Badge, Connecting (FireflySync), Core, Core: components, Engine, Loader, Toast, Tokens
The prompt
the prompt
A form that saves itself, and a footer that never claims more than the network has done.
The prompt
Build a form footer that saves the form when the person pauses typing and says what is happening, using the Susegad UI Loader, Badge, Toast and Connecting components. Keep the logic pure and testable: a state machine that takes the person's edits and a transport's events and returns the next state and the effects to carry out, and a view that turns the state into the footer's words. Drive every state from a transport: a fake one for the demo, pure and seeded, whose saves take a seeded time and fail if the link is down when they start or drops while they are in flight, and whose link comes back through a reconnect handshake. The footer shows "Saving" with the loader only while a save is in flight, then a badge saying "Saved at 14:32" with the time the save finished. If a save fails, the badge says "Not saved", an error toast says "Couldn't save. Check your connection and try again." with a Try again button, and the Connecting indicator shows the link as offline, then connecting while it reconnects. When the link is up again, try once more by itself, clear the error toast, say "You're back online. Your changes are saved.", and let the fireflies be seen in step for a few seconds before the indicator goes. Edits made during a save are saved straight after it. Give the footer one polite status line for its words, keep the pictures hidden from screen readers, and make "Save now" a real submit button with Ctrl+S as a shortcut. Let the page's register choose the pictures: in quiet, three dots that pulse slowly, a hairline badge and a still dot for the link; in warm, a kolam loader, a hand-inked badge and fireflies; in playful, a spinning top, a colour chip and a brighter swarm. With reduced motion nothing moves, and the words carry every state. The typing pause only decides when a save starts; it must never show as progress. Show every state of the footer on the demo page.
Words to code
| When you say | Technique | What happens |
|---|---|---|
| a state machine that takes the person's edits and a transport's events | pure core | reduce(state, event) returns the next state and a list of effects such as "start a save" or "show the error toast". The page carries the effects out; the machine never touches the DOM or a timer, so it is tested with a virtual clock. |
| pure and seeded, whose saves take a seeded time | determinism | Each save's time comes from a seeded generator, and each reconnect handshake's length is seeded by when the link came back, so the same actions at the same times always give the same story. |
| fail if the link is down when they start or drops while they are in flight | failure at the real moment | The transport checks the whole span of each save against the link. A save that started before the drop fails when the drop happens, not before, and not after. |
| only while a save is in flight | motion that follows the work | The loader is tied to the machine's in-flight save, which the transport's answer clears. The test checks this at every 50 ms step of a twenty-second run. |
| with the time the save finished | real state | "Saved at" uses the time in the transport's saved event, formatted as a 24-hour clock, never the time the page last looked. |
| try once more by itself, clear the error toast | recovery | The machine starts a save when the link event says up and the last save failed. One error toast is shown per outage, however many tries fail. |
| Let the page's register choose the pictures | registers | The recipe draws nothing itself. Loader, Badge, Toast and Connecting each read data-register and load only their own skin, so one attribute on the page changes the whole footer. |
| one polite status line for its words | accessibility | A visually hidden role="status" span holds the footer's words. The loader and badge sit in an aria-hidden wrapper, so a screen reader hears each state once. |
A form that saves itself when the person pauses typing, with a footer that says what is happening: "Saving", "Saved at 14:32", or "Couldn't save. Check your connection and try again." When the connection drops, the footer shows the connection's state (fireflies, in warm and playful) and saves by itself once the connection is back.
It composes four library pieces:
| Piece | Used for |
|---|---|
<sg-loader> | the picture while a save is in flight, and only then |
<sg-badge> | the settled state: "No changes yet", "Changes not saved yet", "Saved at 14:32", "Not saved" |
<sg-toast-region> | the error, which waits until dismissed and offers "Try again", and the note that the changes are safe once back online |
<sg-connecting> | the link while it is down, reconnecting, and for four seconds after it is back |
Files
| File | What it is |
|---|---|
transport.js | A fake network, pure and seeded: saves take a seeded time, fail if the link is down when they start or drops while they are in flight, and the link comes back through a reconnect handshake. Swap it for your fetch or websocket. |
footer.core.js | The state machine, the view and the words (STRINGS), pure. Also createSession, which joins the machine, a transport and the typing pause. |
recipe.css | The footer's own styles: its layout, and hidden for the components inside it. The status line uses .sg-vh from tokens.css. |
recipe.js | The wiring: mountSavingFooter(form, { transport, toasts }), plus renderFooter and stillFooter for the gallery. |
index.html | The demo: a live form, a network switch standing in for the world, and every state of the footer. Works in a builder's project too. |
recipe.test.js | Tests for the transport and the machine, driven by a virtual clock. |
Put it on a page
Give your form a footer and mount it:
<link rel="stylesheet" href="susegad/tokens/fonts.css">
<link rel="stylesheet" href="susegad/tokens/tokens.css"> <!-- the tokens, and .sg-vh for the status line -->
<link rel="stylesheet" href="susegad/components/loader/loader.css">
<link rel="stylesheet" href="susegad/components/badge/badge.css">
<link rel="stylesheet" href="susegad/components/toast/toast.css">
<link rel="stylesheet" href="susegad/components/connecting/connecting.css">
<link rel="stylesheet" href="susegad/recipes/saving-footer/recipe.css">
<form id="notes">
<!-- your fields -->
<footer class="saving-footer"></footer>
</form>
<sg-toast-region></sg-toast-region>
<script type="module">
import { mountSavingFooter } from './susegad/recipes/saving-footer/recipe.js';
import { createTransport } from './susegad/recipes/saving-footer/transport.js';
await customElements.whenDefined('sg-toast-region');
mountSavingFooter(document.getElementById('notes'), {
transport: createTransport(), // swap for your own, see below
toasts: document.querySelector('sg-toast-region'),
});
</script>
The CLI also copies index.html, the demo: it links the tokens relatively and reads ?register, ?theme and ?palette, so it opens in your project as it does here.
Use it with a real server
createSession only needs an object with request(at, bytes) returning an id, and poll(now) returning the events that have happened: { type: 'saved' | 'failed', id, at } and { type: 'link', state: 'down' | 'reconnecting' | 'up', at }. Wrap your fetch so a finished request queues a saved event with the time it finished, a failed one queues failed, and online, offline and your reconnect logic queue link events.
What moves, and why
- The footer changes only on the transport's events. There is no timer that pretends: the loader is shown exactly while a save is in flight, and "Saved at" is the time the save finished.
- Edits made during a save are saved straight after it, never dropped.
- One error toast per outage, however many tries fail. When the connection is back, the footer tries again by itself, clears the error and says so.
- The typing pause (1.2 s) only decides when to start a save. It never shows progress.
Accessibility
- One polite status line (
role="status", visually hidden) says the footer's state. The loader and badge are its picture and are hidden from screen readers, so nothing is said twice. - The connection indicator has its own status line ("Connection: offline", "Connection: connecting", "Connection: connected").
- The error toast is spoken through the region's alert line and stays until dismissed or resolved. Its "Try again" button is a real button.
- "Save now" is a real submit button; Ctrl+S (Cmd+S) saves too.
- A screen reader user who types steadily hears "Saving" and "Saved at…" each time they pause. For long writing, consider announcing only failures and a save after a longer pause.
Verify
node --test packages/recipes/saving-footer/recipe.test.js
MSYS_NO_PATHCONV=1 node tools/matrix.mjs --url /packages/recipes/saving-footer/index.html --jobs 1
MSYS_NO_PATHCONV=1 node tools/axe.mjs --url /packages/recipes/saving-footer/index.html