Regsoft design system

Upload

The upload component provides a drag-and-drop file upload zone with automatic progress tracking, file validation, and error display. Files begin uploading immediately when selected or dropped. The component is fully self-contained and does not need to be wrapped in a form.

Required LiveView configuration

Every upload needs two things in your LiveView: an allow_upload/3 call in mount/3 and a handle_event/3 clause for "cancel-upload". Each allow_upload call configures one independent upload — you can have as many as you need on a single page, each with its own file type restrictions, entry limits, and size limits.

Step 1: Configure uploads in mount/3

Call allow_upload/3 for each upload you need. The first argument is an atom name (e.g. :avatar, :documents) that you'll reference in the template via @uploads.name.

Key options:

Option Required? Description
accept Yes File types to accept. Use :any for all types, or a list of extensions like ~w(.pdf .docx), or MIME types like ~w(image/*).
max_entries Yes Maximum number of files that can be uploaded at once. Use 1 for single-file uploads, or a higher number for multi-file.
auto_upload Yes Must be true. This makes files upload immediately when selected or dropped, rather than waiting for a form submit.
progress Yes A function reference like &handle_progress/3. Called on each progress update. You use this to consume files once they finish uploading (see Step 2).
max_file_size No Maximum file size in bytes. Defaults to 8 MB. Example: 1_000_000 for 1 MB.

Example — a page with two independent uploads:

def mount(_params, _session, socket) do
  socket =
    socket
    # Single image upload (max 1 JPG/PNG, up to 5 MB)
    |> allow_upload(:avatar,
      accept: ~w(.jpg .jpeg .png),
      max_entries: 1,
      max_file_size: 5_000_000,
      auto_upload: true,
      progress: &handle_progress/3
    )
    # Multi-file PDF upload (up to 3 files, default 8 MB each)
    |> allow_upload(:documents,
      accept: ~w(.pdf),
      max_entries: 3,
      auto_upload: true,
      progress: &handle_progress/3
    )

  {:ok, socket}
end

Step 2: Write the progress callback

The progress callback is called every time an upload chunk is received. When entry.done? is true, the file has finished uploading and you must call consume_uploaded_entries/3 to process it. Inside the consumer function, you receive a map with the temporary file path and the entry metadata.

defp handle_progress(_upload_name, entry, socket) do
  if entry.done? do
    completed =
      consume_uploaded_entries(socket, entry.upload_config, fn %{path: path}, entry ->
        # Copy the temp file to permanent storage, save to DB, etc.
        #
        # NEVER trust entry.client_name: it is attacker-controlled and may
        # contain path traversal ("../"). Generate a random filename and
        # store uploads OUTSIDE priv/static (which is served publicly, so an
        # uploaded .html would be a stored-XSS vector). upload_dir points at
        # a private directory, e.g. Path.join(System.tmp_dir!(), "uploads").
        ext = entry.client_name |> Path.basename() |> Path.extname()
        dest = Path.join(upload_dir, "#{Ecto.UUID.generate()}#{ext}")
        File.cp!(path, dest)
        {:ok, %{name: entry.client_name, path: dest}}
      end)

    # Do something with the completed files (assign, notify, etc.)
    {:noreply, assign(socket, :uploaded_files, completed)}
  else
    {:noreply, socket}
  end
end
Security: never trust entry.client_name. It is supplied by the client and can contain path traversal (../). Always derive a fresh, random filename and write uploads to a directory outside priv/static — files under priv/static are served publicly, so an uploaded .html becomes a stored-XSS vector.

Step 3: Handle cancel and validate events

The component sends a "cancel-upload" event when a user cancels an in-progress upload. Add this handler to your LiveView:

def handle_event("cancel-upload", %{"ref" => ref, "upload" => upload_name}, socket) do
  {:noreply, cancel_upload(socket, String.to_existing_atom(upload_name), ref)}
end

The component also sends a "validate-upload" event on file selection. You need a handler for this, but it can simply return the socket unchanged:

def handle_event("validate-upload", _params, socket) do
  {:noreply, socket}
end

Step 4: Add the component to your template

Drop <Reg.upload_file> anywhere in your template. No wrapping form is needed — the component is self-contained.

<Reg.upload_file
  id="avatar-upload"
  upload={@uploads.avatar}
  label="Profile photo"
  hint="JPG or PNG, max 5 MB"
/>

<Reg.upload_file
  id="doc-upload"
  upload={@uploads.documents}
  label="Supporting documents"
  hint="PDF files, up to 3"
/>

Component attributes

Attribute Type Default Description
id :string required Unique DOM ID for the upload component
upload UploadConfig required The upload config from @uploads.name
label :string "Upload files" Label text above the dropzone. Set to nil to hide.
hint :string nil Hint text shown inside the dropzone
cancel_event :string "cancel-upload" Event name pushed when cancel button is clicked
class :string nil Additional CSS class for the wrapper

Live examples

Basic single-file upload

A single-file upload accepting any type.

Example

or drag files here

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.

Multi-file upload

Accepts up to 3 files at once.

Example

or drag files here

Up to 3 files

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.

Image-only upload

Restricts to image file types. Attempting to upload non-image files will show a validation error.

Example

or drag files here

JPG, PNG, GIF, or WebP — up to 5 files

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.

Size-constrained upload

Limits file size to 1 MB. Files over 1 MB will trigger a "File is too large" error.

Example

or drag files here

Maximum file size: 1 MB

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.

Custom label

Example

or drag files here

Up to 2 files of any type

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.

No label variant

Pass label= to remove the label entirely. This is useful when the upload sits inside a form section that already has a heading.

Example

or drag files here

Drop files here

Markup

Source markup is unavailable in this environment. Markup extraction relies on debug_heex_annotations and the project source files, which are only present during development.