File upload
LiveView uploads with the chrome built in - drop zones, progress, cancel, avatar and gallery presets. It is ordinary
allow_upload/3
underneath.
The dropzone
Hand it an @uploads.<name> from allow_upload/3 and it renders the whole surface. The hint line is derived from the config, so the accepted types, the size cap and the file count stay true without you repeating them.
<.file_upload
upload={PetalComponents.Showcase.FileUpload.config("sxfu0", [])}
label="Drop your documents here"
/>
Files in flight
Each entry gets a type icon, its humanised size, a progress bar carrying role=progressbar, and a cancel button named after the file. Progress comes straight from entry.progress, so the bar moves as LiveView reports chunks.
-
q3-forecast.pdf
-
board-notes.docx
<.file_upload
upload={PetalComponents.Showcase.FileUpload.uploading_config()}
label="Drop your documents here"
/>
Errors, config level and per entry
upload_errors/1 and upload_errors/2 are rendered as plain English. The config-level message sits above the list and is described from the wrapper; the per-entry one sits inside its row, described from that row's cancel button, so a screen reader reads the file and the problem together when you tab to it.
You have selected too many files
-
raw-scan.pdf This file is too large
-
cover-letter.pdf
<.file_upload
upload={PetalComponents.Showcase.FileUpload.error_config()}
label="Attach up to two files"
/>
Compact
A browse button and the list, no zone. For forms where a full dashed rectangle would shout too loudly. Drag and drop still works, the button is the drop target.
<.file_upload
upload={PetalComponents.Showcase.FileUpload.config("sxfu3", max_entries: 1)}
variant="compact"
label="Attach a file"
/>
Gallery, the edit form
A grid of tiles with the cancel button and progress on the tile itself, plus an add tile that bows out at max_entries. The first two photos are already saved: they come from your database through the :existing slot as plain images, with your own remove event rather than cancel_upload. The third is an upload in flight, and its thumbnail is drawn from the browser's copy of the file by live_img_preview, so on this static page it stays a quiet placeholder with its progress bar until a real LiveView fills it in. The URLs here are inline SVG stand-ins; swap in real photos and nothing else changes.
<.file_upload
upload={PetalComponents.Showcase.FileUpload.gallery_config()}
variant="gallery"
label="Listing photos"
>
<:existing
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 3'><defs><linearGradient id='k' x1='0' y1='0' x2='0.6' y2='1'><stop offset='0' stop-color='%23fde8cd'/><stop offset='1' stop-color='%23a16207'/></linearGradient></defs><rect width='4' height='3' fill='url(%23k)'/></svg>"
name="kitchen.jpg"
remove_event="remove-photo"
remove_value="kitchen"
/>
<:existing
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 3'><defs><linearGradient id='b' x1='0' y1='0' x2='0.4' y2='1'><stop offset='0' stop-color='%23bae6fd'/><stop offset='1' stop-color='%230e7490'/></linearGradient></defs><rect width='4' height='3' fill='url(%23b)'/></svg>"
name="balcony.jpg"
remove_event="remove-photo"
remove_value="balcony"
/>
</.file_upload>
Avatar
One circular target with a replace overlay on hover and on keyboard focus. Empty on the left; on the right the photo already on the account, handed over as a URL through the :existing slot. Either way, picking a file takes over the circle.
<div class="flex flex-wrap items-start gap-8">
<.file_upload
upload={
PetalComponents.Showcase.FileUpload.config("sxfu5",
name: :avatar,
accept: ~w(.png .jpg),
max_entries: 1,
max_file_size: 2_000_000
)
}
variant="avatar"
label="Profile photo"
/>
<.file_upload
upload={
PetalComponents.Showcase.FileUpload.config("sxfu6",
name: :avatar,
accept: ~w(.png .jpg),
max_entries: 1,
max_file_size: 2_000_000
)
}
variant="avatar"
label="Profile photo"
>
<:existing
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'><defs><linearGradient id='a' x1='0' y1='0' x2='0.7' y2='1'><stop offset='0' stop-color='%23c7d2fe'/><stop offset='1' stop-color='%234338ca'/></linearGradient></defs><rect width='1' height='1' fill='url(%23a)'/></svg>"
name="Current photo"
/>
</.file_upload>
</div>
Your own entry row
The :entry slot hands you the %Phoenix.LiveView.UploadEntry{} and replaces the default row outright, so translated copy or a different layout costs one slot. You own the progress and cancel affordances once you take it over.
-
q3-forecast.pdf 100%
-
board-notes.docx 42%
<.file_upload
upload={PetalComponents.Showcase.FileUpload.uploading_config()}
label="Drop your documents here"
>
<:entry :let={entry}>
<div class="flex items-center justify-between w-full gap-3 text-sm">
<span class="font-medium truncate">{entry.client_name}</span>
<span class="text-gray-500 tabular-nums dark:text-gray-400">{entry.progress}%</span>
</div>
</:entry>
</.file_upload>
Properties
| Attribute | Type | Default | Description |
|---|---|---|---|
cancel_event
|
string |
"cancel-upload"
|
phx-click event name emitted by each entry's cancel button. The parent LiveView handles it and calls `cancel_upload/3` with the entry ref, sent as `phx-value-ref`. |
cancel_label
|
string |
"Cancel upload of"
|
prefix for each cancel button's accessible name, joined with the file name |
cancel_target
|
any |
nil
|
phx-target for the cancel event when used inside a LiveComponent |
class
|
any |
nil
|
CSS class for the outer wrapper |
description
|
string |
nil
|
hint line under the label. When nil it is derived from the config - accepted extensions from `:accept`, the cap from `:max_file_size`, the count from `:max_entries` (e.g. "PNG or JPG, up to 8 MB, max 4 files"). Pass "" for no line. |
id
|
string |
nil
|
id for the wrapper; the ARIA relationships are derived from it. Defaults to the upload ref. |
label
|
string |
nil
|
heading text inside the drop zone |
remove_label
|
string |
"Remove"
|
prefix for the accessible name of each `:existing` item's remove button, joined with that item's name |
rest
|
global |
||
upload*
|
{:struct, Phoenix.LiveView.UploadConfig} |
the upload config from `allow_upload/3`, e.g. `@uploads.avatar` | |
variant
|
string |
"dropzone"
|
dropzone = full dashed zone; compact = browse button + list, no zone; avatar = single circular image with a replace overlay; gallery = grid of preview tiles
one of: "dropzone", "compact", "avatar", "gallery"
|
:entry
slot
|
slot |
optional custom rendering for each entry; receives the `%Phoenix.LiveView.UploadEntry{}` and replaces the default entry row | |
:existing
slot
|
slot |
files already uploaded and stored, shown by URL before the in-flight entries. Nothing here touches the upload config - see the module docs |