Ready-to-use Phoenix LiveView components for media uploads and galleries.
These components eliminate the boilerplate of building file upload UIs in Phoenix LiveView. They handle drag-and-drop, progress bars, image previews, error display, and existing media management out of the box.
Setup
Add to your my_app_web.ex html_helpers:
defp html_helpers do
quote do
# ... existing imports
import PhxMediaLibrary.Components
import PhxMediaLibrary.ViewHelpers
end
endQuick Start
# In your LiveView mount:
def mount(_params, _session, socket) do
use PhxMediaLibrary.LiveUpload
post = Posts.get_post!(id)
{:ok,
socket
|> assign(:post, post)
|> allow_media_upload(:images, model: post, collection: :images)
|> stream_existing_media(:media, post, :images)}
end
# In your template:
<.media_upload
upload={@uploads.images}
id="post-images-upload"
/>
<.media_gallery
media={@streams.media}
id="post-gallery"
/>
Summary
Functions
Renders a BlurHash placeholder as a <canvas> element.
Renders a gallery of existing media items with delete support.
Renders a complete media upload zone with drag-and-drop, previews, and progress.
Renders a minimal file input button without the full drop zone.
Renders a <video> player for a media item with an optional poster frame
and a metadata strip showing duration, dimensions, and codec.
Functions
Renders a BlurHash placeholder as a <canvas> element.
The hash is decoded client-side by a colocated JavaScript hook that paints the low-fidelity blurred preview onto the canvas. This is a lightweight alternative to the tiny JPEG placeholder: the hash is ~20–40 bytes stored directly in the database rather than a base64-encoded image.
Requires PhxMediaLibrary.Config.blurhash_enabled?/0 to be true (opt-in
via config :phx_media_library, responsive_images: [blurhash: true]) and the
:image library to be available.
Attributes
:media— (required) aPhxMediaLibrary.Mediastruct. The hash is read frommedia.responsive_images["blurhash"].:width— canvas render width in pixels (default:32). The canvas is stretched to fill its container viawidth: 100%CSS so you can set this to any small value without affecting the visual size.:height— canvas render height in pixels (default:32). If you passnil, the component preserves the aspect ratio from the media metadata.:class— additional CSS classes applied to the<canvas>.
Examples
<%!-- Basic usage --%>
<PhxMediaLibrary.Components.blurhash media={@photo} />
<%!-- Full-bleed cover with aspect-ratio preservation --%>
<div class="relative overflow-hidden rounded-xl">
<PhxMediaLibrary.Components.blurhash
media={@photo}
class="absolute inset-0 w-full h-full object-cover"
/>
<img src={PhxMediaLibrary.url(@photo)} class="relative w-full" loading="lazy" />
</div>Attributes
media(:any) (required)width(:integer) - Defaults to32.height(:integer) - Defaults to32.class(:string) - Defaults tonil.
Renders a gallery of existing media items with delete support.
Designed to work with LiveView streams. Each media item is rendered as a card with a thumbnail (for images) or file icon (for documents), the filename, file size, and a delete button.
Attributes
media(required) — the stream from@streams.mediaid(required) — unique DOM id for the gallery containerclass— additional CSS classesconversion— the conversion to display for image thumbnails (default: nil = original)delete_event— the phx-click event name for deleting (default: "delete_media")delete_target— optional phx-target for the delete eventconfirm_delete— show a JS confirmation before delete (default: true)confirm_message— the confirmation prompt textcolumns— grid column count hint: 2, 3, 4, 5, 6 (default: 4)
Slots
item— override rendering of each media item. Receives the media struct.empty— content to show when the gallery is empty.
Examples
<%!-- Basic usage with streams --%>
<.media_gallery
media={@streams.media}
id="post-gallery"
/>
<%!-- With custom columns and conversion --%>
<.media_gallery
media={@streams.media}
id="post-gallery"
conversion={:thumb}
columns={3}
/>
<%!-- Custom item rendering --%>
<.media_gallery media={@streams.media} id="gallery">
<:item :let={{_id, media}}>
<div class="relative aspect-square rounded-lg overflow-hidden">
<img src={PhxMediaLibrary.url(media, :thumb)} class="object-cover w-full h-full" />
</div>
</:item>
<:empty>
<p class="text-zinc-400 text-center py-12">No media uploaded yet.</p>
</:empty>
</.media_gallery>Attributes
media(:any) (required) - the stream from @streams.id(:string) (required) - unique DOM id for the gallery container.class(:string) - additional CSS classes. Defaults tonil.conversion(:atom) - image conversion for thumbnails. Defaults tonil.delete_event(:string) - event name for delete. Defaults to"delete_media".delete_target(:any) - phx-target for delete event. Defaults tonil.confirm_delete(:boolean) - show confirmation dialog. Defaults totrue.confirm_message(:string) - confirm text. Defaults to"Are you sure you want to delete this file?".columns(:integer) - grid columns (2-6). Defaults to4.
Slots
item- override item rendering — receives {dom_id, media}.empty- content when gallery is empty.
Renders a complete media upload zone with drag-and-drop, previews, and progress.
This component renders:
- A drop zone with a file picker button
- Live image previews for selected files
- Upload progress bars per entry
- Error messages for invalid files
- Cancel buttons for pending uploads
The parent LiveView must call allow_upload/3 or allow_media_upload/3
and handle at minimum a "validate" event (can be a no-op) and a submit
event that calls consume_media/5 or consume_uploaded_entries/3.
Attributes
upload(required) — the upload config from@uploads.nameid(required) — unique DOM id for the componentlabel— text label above the drop zonesublabel— secondary text below the icon (e.g. accepted formats)class— additional CSS classes on the outer wrappercompact— render a compact single-line version (default: false)disabled— disable the upload zone (default: false)cancel_event— the phx-click event name for cancelling an entry (default: "cancel_upload")cancel_target— optional phx-target for the cancel event
Slots
drop_zone— override the default drop zone content entirely
Examples
<%!-- Basic usage --%>
<.media_upload upload={@uploads.images} id="images-upload" />
<%!-- With labels --%>
<.media_upload
upload={@uploads.images}
id="images-upload"
label="Upload Images"
sublabel="JPG, PNG, WebP up to 10MB"
/>
<%!-- Compact variant for single-file uploads --%>
<.media_upload
upload={@uploads.avatar}
id="avatar-upload"
compact={true}
label="Profile Photo"
/>
<%!-- Custom drop zone content --%>
<.media_upload upload={@uploads.docs} id="docs-upload">
<:drop_zone>
<div class="text-center p-8">
<p class="text-lg font-semibold">Drop your documents here</p>
</div>
</:drop_zone>
</.media_upload>Attributes
upload(:any) (required) - the upload config from @uploads.id(:string) (required) - unique DOM id.label(:string) - label text above the drop zone. Defaults tonil.sublabel(:string) - secondary helper text. Defaults tonil.class(:string) - additional CSS classes. Defaults tonil.compact(:boolean) - compact single-line layout. Defaults tofalse.disabled(:boolean) - disable the upload zone. Defaults tofalse.cancel_event(:string) - event name for cancel. Defaults to"cancel_upload".cancel_target(:any) - phx-target for cancel event. Defaults tonil.
Slots
drop_zone- override default drop zone content.
Renders a minimal file input button without the full drop zone.
Useful for embedding uploads inline within forms or other UI elements where the full drop zone is too large.
Attributes
upload(required) — the upload config from@uploads.nameid(required) — unique DOM idlabel— button label text (default: "Choose file")class— additional CSS classes on the wrappericon— hero icon name (default: "hero-arrow-up-tray")
Examples
<.media_upload_button upload={@uploads.avatar} id="avatar-btn" label="Change photo" />Attributes
upload(:any) (required) - the upload config from @uploads.id(:string) (required) - unique DOM id.label(:string) - button label. Defaults to"Choose file".class(:string) - additional CSS classes. Defaults tonil.icon(:string) - icon name. Defaults to"hero-arrow-up-tray".
Renders a <video> player for a media item with an optional poster frame
and a metadata strip showing duration, dimensions, and codec.
Poster frames are generated automatically when FFmpeg is installed and the video was uploaded after v0.6.0. When no poster is available the browser renders its default video thumbnail.
Examples
<PhxMediaLibrary.Components.media_video media={@video} />
<PhxMediaLibrary.Components.media_video
media={@video}
controls={true}
class="rounded-xl shadow-lg"
/>Attributes
media(:any) (required) - APhxMediaLibrary.Mediastruct for a video file.class(:string) - Extra CSS classes for the wrapping div. Defaults tonil.controls(:boolean) - Show native browser video controls. Defaults totrue.autoplay(:boolean) - Auto-play on load. Defaults tofalse.muted(:boolean) - Mute audio on load (required for autoplay in most browsers). Defaults tofalse.loop(:boolean) - Loop the video. Defaults tofalse.