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
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.
Drop files here or click to select
rx.upload(
rx.text("Drop files here or click to select"),
id="upload_basic",
border="2px dashed var(--gray-7)",
padding="2em",
)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
Drop files here or click to select
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")))| Prop | Values | Default | Use |
|---|---|---|---|
id | string (fixed) | "default" | Name of this dropzone. The special Vars and events below use the same id |
accept | {MIME: [extensions...]} | all files | File types to accept |
multiple | True / False | True | Allow picking several files |
max_files | integer | no limit | Maximum number of files |
max_size, min_size | bytes | no limit | Maximum / minimum size of a single file |
disabled | True / False | False | Turn the dropzone off |
no_click, no_drag, no_keyboard | True / False | False | Block clicking / drag and drop / opening with the keyboard |
drag_active_style | style dict | none | Style while files are dragged over it (0.10) |
on_drop | event | keeps the picked files | Runs as soon as files are dropped or picked |
on_drop_rejected | event | error toast | When files that don't meet the conditions come in |
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.
Drag and drop files here or click to select files
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"),
)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.
Drop up to 5 files (2 MB each)
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"),
),
)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./_upload and is public to everyone. It's a good idea to save files with a random prefix so names don't collide.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.
Images or PDF only, up to 3 files
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")),
)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.
Click to upload
or drag and drop
SVG, PNG, JPG or GIF (MAX. 5MB)
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"),
)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.
Drag and drop files here or click to select files
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%",
)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")))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.
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)
Drop or click to attach
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.
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.