Susegad UI
Register
Theme
Palette

Recipes

File upload

Choose photos and PDFs and watch each one arrive. Files too large are refused in plain words beside the field, a dropped file keeps its bar where it stopped and offers Try again, and a Received stamp lands once every file is safe.

quiet warm playful

Open the live demo npx susegad add file-upload

Stands on: Core, Core: components, Empty state, Engine, Field note, File drop, Progress, Stamp, Toast, Tokens

The prompt

the prompt

The kolam closes when the server has the file, not a moment before.

A file uploader for a booking or an enquiry: choose photos and PDFs, see each one arrive, and get a "Received" stamp at the end.

The prompt

Build a file upload flow from Susegad UI components, in plain JavaScript with no framework. Use a native file input with a visible label and a hint that says which kinds of file and how large, linked with aria-describedby. Before any files, show an empty state with a heading, one sentence about what will appear, and a "Choose files" button that opens the input. Check each chosen file in a pure function: refuse files over the size limit or of the wrong kind, and say why in a field note beside the input, naming the file, its size and the limit, and how to fix it. Give every accepted file its own progress bar, a native <progress> inside <sg-progress>, labelled with the file's name and size. Drive the bars only from what the transport reports: write a pure, seeded pretend transport that plans each upload as a short wait, chunks of bytes at a varying rate with the odd slow patch, a pause while the server checks the file, then done, and that can drop a file part way; the page polls it on each animation frame, and only while something is on its way. Set each bar's maximum to the file's bytes plus one step for the server's check, so a bar reads 99% and says "checking the file" after the last byte, and reaches the end, closing the warm register's kolam, only when the server confirms. When a file drops, keep its bar where it stopped, say "didn't upload" in its label, add a "Try again" button to its row, and show an error toast with the same action; dismiss that toast once the file is sent again. When every file has arrived, show one success toast and land a "Received" stamp with the number of files and their total size; lift it again if more files are added. Let the page's register choose every picture, with no extra code: in quiet, hairline bars, a small pencil window in the empty state and a ruled stamp; in warm, a kolam per file, the rain scene beside the empty state's words, inland-letter toasts and a block-print stamp; in playful, chai glasses that fill, a window you can wipe and a postmark on each letter. With reduced motion every piece shows its finished state. Keep it usable by keyboard and screen reader: the native progress elements are named by their labels, "Try again" is a real button named for its file, errors are spoken as alerts and successes politely, and no toast ever takes focus. Keep every string in one object so the words can be edited in one place.

Words to code

When you sayTechniqueWhat happens
Check each chosen file in a pure functionvalidationvet() splits the chosen files into those taken and a note about the rest. It runs in Node, so the rules and the words are tested without a browser.
a pure, seeded pretend transportsimulationEach upload's whole timeline is planned from a seed when it starts, so the same files always upload the same way and tests can step through it with a virtual clock.
only while something is on its waymotion that follows the workThe page's animation frame loop runs while transport.busy() is true and stops after the last event. With nothing moving on the wire, nothing moves on the screen.
the file's bytes plus one step for the server's checkthe server's wordA bar cannot reach 100% on bytes alone. Its last step is the server's confirmation, so "Done" and the closed kolam always mean the file is safe.
keep its bar where it stoppedfailure said plainlyA dropped file keeps the bytes that were reported. The label and the toast say what happened and what to do.
dismiss that toast once the file is sent againtidy stateThe recipe remembers each file's error toast and dismisses it when the file is retried, so no toast outlives the problem it describes.
land a "Received" stampstate as a markThe stamp is in the page from the start, marked pending. It lands, and is read out, only when every file has arrived, and lifts again if more files are added.
Let the page's register choose every pictureregistersThe recipe draws nothing itself. Each component reads data-register and loads only its own skin, so one attribute on the page changes the whole flow, and a quiet page never downloads the scene or the playful skins.
Keep every string in one objectcopySTRINGS in upload.core.js holds every word a person reads, including the error messages with their fixes.

Choose photos and PDFs, watch each one upload, and see a "Received" stamp when every file has arrived. A file that is too large is refused in plain words beside the field. A file that drops part way says so, keeps the bar where it stopped, and offers "Try again".

It composes five library pieces:

PieceUsed for
<sg-empty>before any files: what will appear here, and a "Choose files" action
<sg-field-note>beside the file field: a file too large, or of the wrong kind, and why it was not added
<sg-progress>one per file, driven only by the bytes the transport reports. In warm the kolam closes only when the server confirms the file
<sg-toast-region>an error per dropped file, with "Try again", and one success note when everything has arrived
<sg-stamp>"Received, 3 files, 5.7 MB", landed once every file is safe

Files

FileWhat it is
transport.jsA pretend upload service, pure and seeded: a short wait, chunks of bytes at a varying rate with the odd slow patch, a pause while the server checks the file, then done. It can drop a file part way. Swap it for your own.
upload.core.jsThe words (STRINGS), the rules (size and kind), and the session: files, the transport's events, and what the page must say next. Pure.
recipe.jsThe wiring: mountFileUpload(root, { transport, maxBytes, accept, toasts }), plus freezeAt(t) to show a moment for a gallery.
recipe.cssLayout only; each piece brings its own look.
index.htmlThe demo: a live uploader with sample files, and three frozen moments. It is copied with the recipe and works in your project: open it from any static server, and add ?register=, ?theme= or ?palette= to try the looks.
recipe.test.jsTests for the transport and the session, driven by a virtual clock.

Use it with a real server

The session needs an object with three methods:

  • start(file, at) returns an id and begins sending;
  • poll(now) returns what has happened since the last poll, as { type: 'progress', id, loaded, total, at }, { type: 'done', id, at } or { type: 'failed', id, at, loaded };
  • busy() says whether anything is still on its way.

Wrap XMLHttpRequest (its upload.onprogress gives loaded and total) or a fetch with a streamed body. Queue a progress event for each report, done when the server answers that it has the file, and failed on an error or abort.

What moves, and why

  • A bar shows only bytes the transport has reported. The page's clock runs only while something is on its way; when nothing is moving, nothing is drawn.
  • Each bar's maximum is the file's bytes plus one step for the server's check. When the last byte has left, the bar reads 99% and the label says "checking the file". It reaches the end, and the warm kolam closes, only when the server says the file is safe.
  • A dropped file keeps its bar where it stopped. Nothing pretends it finished.
  • The stamp lands only when every file chosen has arrived. Add more files and it lifts until they have arrived too.
  • One success toast each time everything so far has arrived; one error toast per dropped file, dismissed as soon as that file is sent again.

Accessibility

  • The file field is the browser's own <input type="file"> with a visible label, and the hint is linked with aria-describedby. The field note joins it while it shows an error.
  • Each file's progress is a native <progress> named by the file name and size. Screen readers hear the value from the element itself.
  • "Try again" is a real button in the file's row, named for its file ("Try room-rates-2026.pdf again"). The error toast has the same action.
  • Errors are spoken through the toast region's alert line; the success note and the stamp through polite status lines.
  • The empty state's words and "Choose files" action are plain HTML in reading order.

Try it

Serve the repo (node tools/serve.mjs) and open /packages/recipes/file-upload/index.html, or open the copied index.html in your own project. Add ?register=quiet, warm or playful, and &theme=dark. The sample set includes a 14.2 MB photo, which is refused, and a PDF that drops half way on its first try.