Summary#
Upload is a file picker you supply the appearance of. Put anything inside it — a button, a row with an icon,
a whole drop zone — and it becomes the thing a person clicks to choose a file.
When they choose one, the bytes are stored and your action is called with an UploadedFile:
| field | what it is |
|---|---|
Path | where the bytes were stored. This is the value you keep. |
FileName | the original name the browser reported |
ContentType | the type the browser reported |
Length | the size in bytes, as the browser reported it |
The same value comes back from camera and microphone's Camera.Capture() and both StopRecording()s, so one
Ingest(UploadedFile f) serves a chosen file and a photograph alike.
Signature#
Upload(onUploaded: Ingest) { Text("Choose a file"); } // your own content is the button
Upload(accept: "image/*", onUploaded: Ingest) { … } // narrow what the picker offersDescription#
The content inside it IS the button#
There is no built-in appearance to override. Whatever you render inside the Upload is what a person sees and
clicks — so it takes your icons, your spacing and your variants like anything else.
[Composable] component UploadButton(Action<UploadedFile> onUploaded) {
variants {
base { Display = Display.InlineFlex; H = "32px"; Px = 3; Bg = Colors.Primary; Color = Colors.OnPrimary; Rounded = Radius.Md;
FontSize = FontSize.Body; FontWeight = FontWeight.Semibold; WhiteSpace = WhiteSpace.Nowrap; Transition = Motion.Fast; }
}
render {
Upload(onUploaded: onUploaded) {
Row(gap: 2, align: Align.Center, justify: Justify.Center) { Icon(Icons.Upload, size: 18); Text("Upload"); }
}
}
}The bytes never reach your code#
By the time your action runs, the file is already stored. You receive a path, not a buffer — so a 9 MB photograph costs your action nothing, and there is no way to accidentally hold one in a field.
What you do with the path is the interesting part: usually build a FileAsset from it, which is what gives the
file an owner, grants, deduplication and a quota. demo/file-manager is the worked version.
What may be uploaded#
Images, audio, video, and a few text formats (text/plain, text/markdown, text/csv, application/json). The
limit is 10 MB per file.
⛔ SVG is deliberately refused, even though it is an image. An SVG is a document that can carry script, so a stored one served back from your own origin is a cross-site-scripting vector; every other image format is inert. If you want vector art in your app, ship it as an asset instead — see textures for which folder each kind of art belongs in.
Uploading is an authorized act#
The file store accepts writes from a signed-in user. An app with no users can show a picker and keep nothing — and the refusal arrives as an error your action can catch and report, not as a silent nothing.
This is the same rule camera and microphone runs into, and for the same reason: a file that arrives from a browser has an owner, and an app with no users has nobody to be one.
accept: narrows the picker, it does not enforce anything#
accept: "image/*" tells the browser which files to offer by default. It is a convenience for the person choosing,
not a guarantee — the server decides what it will actually store, and refuses everything outside the list above.
See also#
- camera and microphone — the camera and microphone, which answer the same
UploadedFile - Files (addressing something the app stores) — what to do with the path once you have it
- textures — art the app SHIPS, which is a different thing from a file a person uploads
- component — actions, and where an
onUploadedhandler lives