El taller de componentes: los 100 componentes de Reflex
·
Resumen del curso
Diseño
·
Box
·
Flex
·
Stack
·
Grid
·
Container
·
Section
·
Center
·
Spacer
·
Separator
·
Card
·
Inset
·
Aspect Ratio
·
Fragment
Tipografía
·
Text
·
Heading
·
Link
·
Code
·
Markdown
·
Blockquote
·
Quote
·
Strong
·
Em
·
Kbd
Formularios
·
Button
·
Input
·
Text Area
·
Checkbox
·
Switch
·
Radio Group
·
Select
·
Slider
·
Form
·
Upload
Visualización de datos
·
Badge
·
Avatar
·
Icon
·
Callout
·
List
·
Data List
·
Code Block
·
Progress
·
Spinner
·
Scroll Area
·
Moment
Mostrar y ocultar
·
Tabs
·
Accordion
·
Segmented Control
Superposiciones
·
Tooltip
·
Popover
·
Hover Card
·
Dialog
·
Alert Dialog
·
Drawer
·
Dropdown Menu
·
Context Menu
·
Toast
Renderizado dinámico
·
Cond
·
Match
·
Foreach
·
Auto Scroll
Multimedia
·
Image
·
Audio
·
Video
Tablas y cuadrículas de datos
·
Table
·
Data Table
·
Data Editor
Gráficos
·
Area Chart
·
Bar Chart
·
Line Chart
·
Composed Chart
·
Pie Chart
·
Radar Chart
·
Radial Bar Chart
·
Scatter Chart
·
Funnel Chart
·
Treemap
·
Sankey Chart
·
Error Bar
Piezas de gráficos
·
Axis
·
Cartesian Grid
·
Chart Tooltip
·
Legend
·
Label
·
Reference
·
Brush
Otras bibliotecas de gráficos
·
Plotly
·
Pyplot
Elementos HTML
·
HTML
·
HTML Text
·
HTML Layout
·
HTML Forms
·
HTML Media
·
HTML Tables
·
SVG
Otras herramientas
·
Theme
·
Skeleton
·
Clipboard
·
Html Embed
·
Script
·
Memo
Pedido del taller
Añade a la pantalla de solicitud de reembolso una zona para adjuntar fotos del recibo. Hasta 3 imágenes, que se añadan nada más soltarlas y muestren su nombre y tamaño.
rx.upload es una zona (dropzone) donde se sueltan archivos arrastrándolos o se eligen haciendo clic. Los archivos elegidos se quedan primero en el navegador y solo van al servidor cuando pasas rx.upload_files() a un event handler. El handler del servidor recibe la lista de archivos y los lee o los guarda.
Los ejemplos de este curso se ejecutan en un servidor compartido, así que no guardan los archivos: solo muestran su nombre y tamaño. El código para guardarlos se muestra aparte, solo como código.
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",
)Estas propiedades deciden cómo se comporta la dropzone. Elige algunos archivos y luego desactiva multiple o activa no_click. Los nombres de los archivos elegidos aparecen abajo como badges.
Prueba las propiedades
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")))| Propiedad | Valores | Por defecto | Uso |
|---|---|---|---|
id | Cadena (fija) | "default" | Nombre de esta dropzone. Los Var y eventos especiales de abajo usan el mismo id |
accept | {MIME: [extensiones...]} | Todos los archivos | Tipos de archivo que se aceptan |
multiple | True / False | True | Elegir varios |
max_files | Entero | Sin límite | Número máximo de archivos |
max_size, min_size | Bytes | Sin límite | Tamaño máximo y mínimo de cada archivo |
disabled | True / False | False | Desactivar la dropzone |
no_click, no_drag, no_keyboard | True / False | False | Impedir clic / arrastrar y soltar / abrir con el teclado |
drag_active_style | Dict de estilos | Ninguno | Estilo mientras se arrastra un archivo encima (0.10) |
on_drop | Evento | Guarda los archivos elegidos | Se ejecuta en cuanto se sueltan o eligen archivos |
on_drop_rejected | Evento | Toast de error | Cuando entra un archivo que no cumple las condiciones |
Al elegir archivos, entran en una lista del navegador. Con el Var especial rx.selected_files(id) se muestra en pantalla la lista de nombres, y con el evento especial rx.clear_selected_files(id) se vacía. Todavía no se ha enviado nada al servidor.
id es la clave que une la dropzone con los Var y eventos especiales, así que debe ser el mismo valor en todos. Escríbelo como cadena o constante de módulo; no se puede usar una variable de State. El botón "Select File" de dentro de la dropzone es solo decorativo: hagas clic donde hagas clic dentro de la dropzone, se abre la ventana para elegir archivos.
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"),
)Si pasas rx.upload_files(upload_id=id) al handler, los archivos elegidos van al servidor. El handler es un evento async normal que recibe un único files: list[rx.UploadFile]. De cada archivo puedes usar file.name (nombre original), file.size (bytes) y await file.read() (contenido).
Al handler de subida no se le pone @rx.event(background=True) ni se usa async with self; eso solo es para la subida por fragmentos de más abajo. Y el botón de subida va fuera de la dropzone: si lo pones dentro, cada clic abre la ventana de archivos y además inicia la subida.
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"),
),
)En una app real se guardan en la carpeta que devuelve rx.get_upload_dir() (un Path, por defecto ./uploaded_files, configurable con la variable de entorno REFLEX_UPLOADED_FILES_DIR), y en pantalla se construye la dirección con rx.get_upload_url(nombre_de_archivo) para pasarla a rx.image u otro componente. Abajo está el ejemplo básico de la documentación oficial, que esta página no ejecuta.
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(): la ruta donde guardar en el servidor (backend). Se usa en event handlers.rx.get_upload_url(name): la dirección que usa la pantalla (frontend). No es una cadena de Python sino un Var de frontend, así que solo se usa en código de componentes. La dirección tiene la forma /_upload/nombre_de_archivo, pero no la escribas a mano: pasa siempre por esta función./_upload y es accesible para cualquiera. Conviene guardar los archivos con un prefijo aleatorio para que no choquen nombres iguales.Si conectas un handler a on_drop, los archivos se suben en cuanto se eligen, sin botón. Con accept se filtra el tipo de archivo, y con max_files y max_size, el número y el tamaño. Los archivos que no cumplen las condiciones llegan a on_drop_rejected; si no das ese evento, 0.10 muestra un toast de error. Prueba a soltar abajo un archivo que no sea imagen ni PDF, o uno de más de 2MB.
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 es una dropzone con borde y relleno por defecto. Para diseñar el aspecto desde cero, usa rx.upload.root. Las propiedades y los eventos son los mismos. Con drag_active_style puedes cambiar el fondo mientras se arrastra un archivo encima.
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 (y rx.upload_files_chunk) aceptan on_upload_progress. Durante la subida, a ese handler llega un dict {"loaded": bytes enviados, "total": bytes totales, "progress": 0~1}. rx.cancel_upload(id) detiene una subida en curso. Esta página solo acepta archivos de hasta 2MB, así que el progreso llega a 100 enseguida.
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() carga el archivo entero en memoria. Los archivos grandes se reciben por partes con rx.upload_files_chunk(upload_id=...) y se escriben directamente en disco. El handler de fragmentos debe ser @rx.event(background=True), recibir chunk_iter: rx.UploadChunkIterator y leerlo hasta el final (si sale antes, la subida falla). Cada fragmento tiene chunk.filename, chunk.offset, chunk.content_type y chunk.data. Como es código que escribe en disco, aquí no se ejecuta.
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")))Los archivos no llegan a on_submit del formulario. Si hay una dropzone dentro del formulario, los archivos se envían aparte con rx.upload_files() desde un botón type="button" o desde on_drop, y solo el resto de casillas se reciben con on_submit.
La dropzone filtra primero el tipo de imagen, el número y el tamaño, y con on_drop el servidor recibe el nombre y el tamaño en cuanto se sueltan. La lista solo conserva los tres últimos. No se guarda nada.
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",
)Notas del taller
Dropzone con rx.upload(id="id fijo"). Archivos elegidos: rx.selected_files(id); vaciar: rx.clear_selected_files(id).
Para enviar, handler(rx.upload_files(upload_id=id)). El handler es async y recibe files: list[rx.UploadFile].
Con on_drop se suben al elegirlos. Se filtra con accept · max_files · max_size, y los rechazados van a on_drop_rejected.
Guardar: rx.get_upload_dir(); mostrar: rx.get_upload_url(). La carpeta de subidas es pública.
El botón de subida, fuera de la dropzone. Archivos grandes: rx.upload_files_chunk + handler background.
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
R
ReflexKR
Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.
ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café
Aviso de afiliados© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Preguntas y respuestas
Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.