R

ReflexKR

Reflex Docs

The Component Workshop: Reflex's 100 Library Components

·

Course overview

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typography

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Forms

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Data display

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Disclosure

·

Tabs

·

Accordion

·

Segmented Control

Overlay

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamic rendering

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Media

·

Image

·

Audio

·

Video

Tables and data grids

·

Table

·

Data Table

·

Data Editor

Charts

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Chart parts

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Other chart libraries

·

Plotly

·

Pyplot

HTML elements

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Other tools

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

The Component Workshop: Reflex's 100 Library Components
Forms

Upload

rx.uploadrx.upload.root

Official docs

Workshop order

Add a receipt photo attachment area to the refund request screen. Up to 3 images; dropping them attaches them right away, showing each file's name and size.

rx.upload is an area where you drop files or click to pick them (a dropzone). Picked files first stay in the browser; they go to the server when you pass rx.upload_files() to an event handler. The handler on the server receives the list of files and reads or saves them.

The examples in this course run on a shared server, so they don't save any files; they only show names and sizes. The code that saves files is shown separately, as code only.

rx.upload(
    rx.text("Drop files here or click to select"),
    id="upload_basic",
    border="2px dashed var(--gray-7)",
    padding="2em",
)

Try the props

These props control how the dropzone behaves. Pick a few files, then turn off multiple or turn on no_click. The names of the picked files appear below as badges.

Try the props

label

multiple

disabled

no_click

no_drag

no_keyboard

rx.vstack(rx.upload(rx.text("Drop files here or click to select"), id="upload_lab", multiple=True, disabled=False, no_click=False, no_drag=False, no_keyboard=False, border="2px dashed var(--gray-7)", padding="2em"), rx.foreach(rx.selected_files("upload_lab"), lambda f: rx.badge(f, variant="soft")))
no_click blocks picking by clicking, no_drag blocks drag and drop, and no_keyboard blocks opening with Space or Enter. In 0.10 multiple defaults to True.
PropValuesDefaultUse
idstring (fixed)"default"Name of this dropzone. The special Vars and events below use the same id
accept{MIME: [extensions...]}all filesFile types to accept
multipleTrue / FalseTrueAllow picking several files
max_filesintegerno limitMaximum number of files
max_size, min_sizebytesno limitMaximum / minimum size of a single file
disabledTrue / FalseFalseTurn the dropzone off
no_click, no_drag, no_keyboardTrue / FalseFalseBlock clicking / drag and drop / opening with the keyboard
drag_active_stylestyle dictnoneStyle while files are dragged over it (0.10)
on_dropeventkeeps the picked filesRuns as soon as files are dropped or picked
on_drop_rejectedeventerror toastWhen files that don't meet the conditions come in

Showing picked files: selected_files

Picked files go into a list in the browser. The special Var rx.selected_files(id) shows their names on screen, and the special event rx.clear_selected_files(id) empties the list. Nothing has gone to the server yet.

The id is the key that ties the dropzone to the special Vars and events, so they must all use the same value. Write it as a string or a module constant; a State variable won't work. The "Select File" button inside the dropzone is just for looks: clicking anywhere inside the dropzone opens the file picker anyway.

rx.vstack(
    rx.upload(
        rx.vstack(
            rx.button("Select File", variant="outline"),
            rx.text("Drag and drop files here or click to select files", size="2"),
            align="center",
        ),
        id="upload_selected",
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_selected"), lambda f: rx.badge(f)), wrap="wrap"),
    rx.button("Clear", on_click=rx.clear_selected_files("upload_selected"), variant="soft", color_scheme="gray"),
)

Sending to the server: rx.upload_files

Pass rx.upload_files(upload_id=id) to a handler and the picked files go to the server. The handler is an ordinary async event that takes a single files: list[rx.UploadFile]. For each file you can use file.name (the original name), file.size (bytes) and await file.read() (the content).

Don't put @rx.event(background=True) on an upload handler, and don't use async with self either; those are only for the chunked upload below. And put the upload button outside the dropzone. Inside it, every click would open the file picker as well as start the upload.

def upload_size_label(size: int) -> str:
    return f"{size / 1024:.1f} KB" if size < 1024 * 1024 else f"{size / 1024 / 1024:.2f} MB"


class UploadInfoState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        # Read only the name and size. Nothing is saved to disk.
        self.files = [{"name": str(f.name), "size": upload_size_label(f.size or 0)} for f in files]


rx.vstack(
    rx.upload(
        rx.text("Drop up to 5 files (2 MB each)"),
        id="upload_info",
        max_files=5,
        max_size=2 * 1024 * 1024,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_info"), lambda f: rx.badge(f, variant="soft")), wrap="wrap"),
    rx.hstack(
        rx.button("Upload", on_click=UploadInfoState.handle_upload(rx.upload_files(upload_id="upload_info"))),
        rx.button("Clear", on_click=rx.clear_selected_files("upload_info"), variant="soft", color_scheme="gray"),
    ),
    rx.foreach(
        UploadInfoState.files,
        lambda f: rx.hstack(rx.icon("file", size=14), rx.text(f["name"], size="2"), rx.code(f["size"]), align="center"),
    ),
)

Saving and displaying (code only)

In a real app, you save into the folder returned by rx.get_upload_dir() (a Path; ./uploaded_files by default, changeable with the environment variable REFLEX_UPLOADED_FILES_DIR), and on screen you build the address with rx.get_upload_url(file_name) and pass it to rx.image and the like. Below is the basic example from the official docs; it isn't run on this page.

class State(rx.State):
    uploaded_files: list[str] = []

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        for file in files:
            data = await file.read()
            path = rx.get_upload_dir() / file.name
            with path.open("wb") as f:
                f.write(data)
            self.uploaded_files.append(file.name)


def upload_component():
    return rx.vstack(
        rx.upload(id="upload"),
        rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))),
        rx.foreach(
            State.uploaded_files,
            lambda f: rx.image(src=rx.get_upload_url(f), alt="Uploaded file preview"),
        ),
    )
  • rx.get_upload_dir(): the path to save to on the server (backend). Use it in event handlers.
  • rx.get_upload_url(name): the address to use on screen (frontend). It's a frontend Var, not a Python string, so use it only in component code. The address has the form /_upload/file_name, but go through this function instead of writing it yourself.
  • The upload folder is served at /_upload and is public to everyone. It's a good idea to save files with a random prefix so names don't collide.
  • On hosting such as Reflex Cloud, the upload folder is wiped on every deploy. Keep long-lived files in external storage such as S3.

Uploading in one go: on_drop and filtering

Connect a handler to on_drop and files upload as soon as they are picked, with no button. Filter file types with accept, and count and size with max_files and max_size. Files that don't meet the conditions go to on_drop_rejected; if you don't provide that event, 0.10 shows an error toast. Try dropping a file that isn't an image or PDF, or one larger than 2 MB, below.

class UploadDropState(rx.State):
    names: list[str] = []
    rejected: int = 0

    @rx.event
    async def handle_drop(self, files: list[rx.UploadFile]):
        self.names = [f"{f.name} ({f.size or 0} bytes)" for f in files]

    @rx.event
    def handle_rejected(self, rejections: list):
        self.rejected = len(rejections)


rx.vstack(
    rx.upload(
        rx.text("Images or PDF only, up to 3 files", size="2"),
        id="upload_drop",
        multiple=True,
        accept={
            "application/pdf": [".pdf"],
            "image/png": [".png"],
            "image/jpeg": [".jpg", ".jpeg"],
            "image/webp": [".webp"],
        },
        max_files=3,
        max_size=2 * 1024 * 1024,
        no_keyboard=True,
        on_drop=UploadDropState.handle_drop(rx.upload_files(upload_id="upload_drop")),
        on_drop_rejected=UploadDropState.handle_rejected,
        border="2px dashed var(--gray-7)",
        padding="2em",
    ),
    rx.foreach(UploadDropState.names, lambda n: rx.text(n, size="2")),
    rx.cond(UploadDropState.rejected > 0, rx.text("Rejected: ", UploadDropState.rejected, size="2", color_scheme="red")),
)

An unstyled dropzone: rx.upload.root

rx.upload is a dropzone with a default border and padding. To build the look from scratch, use rx.upload.root. Props and events are the same. drag_active_style changes the background while files are being dragged over it.

rx.vstack(
    rx.upload.root(
        rx.box(
            rx.icon("cloud-upload", size=40, color=rx.color("indigo", 9)),
            rx.hstack(rx.text("Click to upload", weight="bold", color=rx.color("indigo", 11)), rx.text("or drag and drop"), size="2"),
            rx.text("SVG, PNG, JPG or GIF (MAX. 5MB)", size="1", color=rx.color("gray", 10)),
            display="flex",
            flex_direction="column",
            align_items="center",
            gap="6px",
            padding="1.5rem",
            text_align="center",
        ),
        id="upload_root_demo",
        max_width="24rem",
        border=f"2px dashed {rx.color('indigo', 7)}",
        border_radius="12px",
        cursor="pointer",
        drag_active_style={"background_color": rx.color("indigo", 3)},
    ),
    rx.hstack(rx.foreach(rx.selected_files("upload_root_demo"), lambda f: rx.badge(f)), wrap="wrap"),
)

Progress and cancel

rx.upload_files (and rx.upload_files_chunk) accept on_upload_progress. While uploading, this handler receives a dict {"loaded": bytes sent, "total": total bytes, "progress": 0–1}. rx.cancel_upload(id) stops an upload in progress. This page only accepts files up to 2 MB, so progress reaches 100 quickly.

Total bytes received: 0

class UploadProgressState(rx.State):
    uploading: bool = False
    progress: int = 0
    total_bytes: int = 0

    @rx.event
    async def handle_upload(self, files: list[rx.UploadFile]):
        self.total_bytes = sum(f.size or 0 for f in files)

    @rx.event
    def handle_upload_progress(self, progress: dict):
        self.uploading = True
        self.progress = round(progress["progress"] * 100)
        if self.progress >= 100:
            self.uploading = False

    @rx.event
    def cancel_upload(self):
        self.uploading = False
        return rx.cancel_upload("upload_progress")


rx.vstack(
    rx.upload(
        rx.text("Drag and drop files here or click to select files", size="2"),
        id="upload_progress",
        max_size=2 * 1024 * 1024,
        border="1px dotted var(--indigo-8)",
        padding="2em",
    ),
    rx.vstack(rx.foreach(rx.selected_files("upload_progress"), lambda f: rx.text(f, size="2"))),
    rx.progress(value=UploadProgressState.progress, max=100),
    rx.cond(
        ~UploadProgressState.uploading,
        rx.button(
            "Upload",
            on_click=UploadProgressState.handle_upload(
                rx.upload_files(
                    upload_id="upload_progress",
                    on_upload_progress=UploadProgressState.handle_upload_progress,
                ),
            ),
        ),
        rx.button("Cancel", on_click=UploadProgressState.cancel_upload, color_scheme="red"),
    ),
    rx.text("Total bytes received: ", UploadProgressState.total_bytes, size="2"),
    width="100%",
)

Large files: chunked upload (code only)

await file.read() loads the whole file into memory. For large files, receive them piece by piece with rx.upload_files_chunk(upload_id=...) and write straight to disk. A chunk handler must be @rx.event(background=True), take chunk_iter: rx.UploadChunkIterator, and read it to the end (returning early makes the upload fail). Each chunk has chunk.filename, chunk.offset, chunk.content_type and chunk.data. Since this code writes to disk, it isn't run here.

class ChunkUploadState(rx.State):
    uploaded_files: list[str] = []

    @rx.event(background=True)
    async def handle_large_upload(self, chunk_iter: rx.UploadChunkIterator):
        file_handles = {}
        try:
            async for chunk in chunk_iter:  # must consume every chunk
                fh = file_handles.get(chunk.filename)
                if fh is None:
                    path = rx.get_upload_dir() / "stream" / chunk.filename
                    path.parent.mkdir(parents=True, exist_ok=True)
                    fh = file_handles[chunk.filename] = path.open("wb")
                fh.seek(chunk.offset)
                fh.write(chunk.data)
        finally:
            for fh in file_handles.values():
                fh.close()
        async with self:
            self.uploaded_files = sorted(file_handles)


# rx.button("Upload Large Files",
#           on_click=ChunkUploadState.handle_large_upload(rx.upload_files_chunk(upload_id="large_upload")))

Uploads inside a form

Files are not included in a form's on_submit. If there is a dropzone inside the form, send the files separately with rx.upload_files() from a type="button" button or from on_drop, and receive only the other fields through on_submit.

Filling the order

The dropzone filters image type, count and size first, and with on_drop the server receives the names and sizes as soon as the files are dropped. The list keeps only the three most recent. Nothing is saved.

Receipt photos (max 3, 2 MB each)

class UploadReceiptState(rx.State):
    files: list[dict[str, str]] = []

    @rx.event
    async def attach(self, files: list[rx.UploadFile]):
        added = [{"name": str(f.name), "size": f"{(f.size or 0) / 1024:.0f} KB"} for f in files]
        self.files = (self.files + added)[-3:]

    @rx.event
    def clear(self):
        self.files = []


rx.card(
    rx.vstack(
        rx.text("Receipt photos (max 3, 2 MB each)", weight="bold", size="2"),
        rx.upload(
            rx.hstack(rx.icon("image-plus", size=18), rx.text("Drop or click to attach", size="2"), align="center"),
            id="upload_receipt",
            accept={"image/png": [".png"], "image/jpeg": [".jpg", ".jpeg"]},
            max_files=3,
            max_size=2 * 1024 * 1024,
            on_drop=UploadReceiptState.attach(rx.upload_files(upload_id="upload_receipt")),
            border="1px dashed var(--gray-8)",
            border_radius="8px",
            padding="1em",
            width="100%",
            drag_active_style={"background_color": "var(--accent-3)"},
        ),
        rx.foreach(
            UploadReceiptState.files,
            lambda f: rx.hstack(rx.icon("image", size=14), rx.text(f["name"], size="2"), rx.spacer(), rx.code(f["size"]), width="100%", align="center"),
        ),
        rx.button("Remove all", on_click=UploadReceiptState.clear, size="1", variant="soft", color_scheme="gray"),
        width="100%",
    ),
    width="100%",
    max_width="380px",
)

Workshop notes

  • rx.upload(id="fixed id") makes a dropzone. Picked files: rx.selected_files(id); clear them: rx.clear_selected_files(id).

  • Send them from a handler (rx.upload_files(upload_id=id)). The handler is async and takes files: list[rx.UploadFile].

  • With on_drop, files upload as soon as they are picked. Filter with accept, max_files and max_size; rejects go to on_drop_rejected.

  • Save with rx.get_upload_dir(), display with rx.get_upload_url(). The upload folder is public.

  • Keep the upload button outside the dropzone. For large files, use rx.upload_files_chunk + a background handler.

← Previous

Forms · Form

Next →

Data display · Badge

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

R

ReflexKR

Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.

ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee

Affiliate disclosure
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex