Attachment

Displays a file or image attachment with media, metadata, upload state, and actions.

Installation

Copy the following code into your app directory.

uv

uv run buridan add component attachment
from components.ui.attachment import attachment

Anatomy

Use the following composition to build an Attachment component.

attachment.root( attachment.media(), attachment.content( attachment.title(), attachment.description(), ), attachment.actions( attachment.action() ), )

Features

  • Icon and image media through attachment.media

  • Upload states: idle, uploading, processing, error, and done with built-in styling and a shimmer while in progress

  • Three sizes and horizontal or vertical orientation

  • A full-card attachment.trigger that opens a link or dialog while the actions stay independently clickable

  • Scrollable, snapping attachment.group with an edge fade

  • Customizable styling through the class_name prop on every part

Examples

Image

Set variant="image" on attachment.media and render an rx.el.img() inside it. Use orientation="vertical" to stack the media above the content.

Workspace
workspace.pngPNG · 820 KB
Desk
desk-reference.jpgJPG · 1.1 MB
Office
office-reference.jpgJPG · 940 KB
import reflex as rx

from components.icons.hugeicon import hi
from components.ui.attachment import attachment

images = [
    {
        "name": "workspace.png",
        "meta": "PNG · 820 KB",
        "src": "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
        "alt": "Workspace",
    },
    {
        "name": "desk-reference.jpg",
        "meta": "JPG · 1.1 MB",
        "src": "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
        "alt": "Desk",
    },
    {
        "name": "office-reference.jpg",
        "meta": "JPG · 940 KB",
        "src": "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
        "alt": "Office",
    },
]


def attachment_image_demo():
    return rx.el.div(
        attachment.group(
            rx.foreach(
                images,
                lambda image: attachment.root(
                    attachment.trigger(
                        link=True,
                        href=image["src"],
                        target="_blank",
                        rel="noreferrer",
                        aria_label=f"Open {image['name']}",
                    ),
                    attachment.media(
                        rx.el.img(src=image["src"], alt=image["alt"]),
                        variant="image",
                    ),
                    attachment.content(
                        attachment.title(image["name"]),
                        attachment.description(image["meta"]),
                    ),
                    attachment.actions(
                        attachment.action(
                            hi("Cancel01Icon"),
                            aria_label=f"Remove {image['name']}",
                        )
                    ),
                    orientation="vertical",
                ),
            ),
            class_name="w-full",
        ),
        class_name="mx-auto w-full max-w-sm py-12",
    )

States

Set state to reflect the upload lifecycle. uploading and processing shimmer the title, and error switches to a destructive treatment.

selected-file.pdfReady to upload
design-system.zipUploading · 64%
market-research.pdfProcessing document
financial-model.xlsxUpload failed. Try again.
uploaded-report.pdfUploaded · 1.8 MB
import reflex as rx

from components.icons.hugeicon import hi
from components.ui.attachment import attachment
from components.ui.spinner import spinner


def attachment_states_demo():
    return rx.el.div(
        attachment.root(
            attachment.media(hi("Clock01Icon")),
            attachment.content(
                attachment.title("selected-file.pdf"),
                attachment.description("Ready to upload"),
            ),
            attachment.actions(
                attachment.action(
                    hi("Cancel01Icon"), aria_label="Remove selected-file.pdf"
                )
            ),
            state="idle",
        ),
        attachment.root(
            attachment.media(spinner()),
            attachment.content(
                attachment.title(
                    "design-system.zip",
                    class_name="shimmer",
                ),
                attachment.description("Uploading · 64%"),
            ),
            attachment.actions(
                attachment.action(hi("Cancel01Icon"), aria_label="Cancel upload")
            ),
            state="uploading",
        ),
        attachment.root(
            attachment.media(hi("File02Icon")),
            attachment.content(
                attachment.title("market-research.pdf"),
                attachment.description("Processing document"),
            ),
            attachment.actions(
                attachment.action(
                    hi("Cancel01Icon"), aria_label="Remove market-research.pdf"
                )
            ),
            state="processing",
        ),
        attachment.root(
            attachment.media(hi("FileExclamationPointIcon")),
            attachment.content(
                attachment.title("financial-model.xlsx"),
                attachment.description("Upload failed. Try again."),
            ),
            attachment.actions(
                attachment.action(hi("RefreshIcon"), aria_label="Retry upload"),
                attachment.action(
                    hi("Cancel01Icon"), aria_label="Remove financial-model.xlsx"
                ),
            ),
            state="error",
        ),
        attachment.root(
            attachment.media(hi("Tick02Icon")),
            attachment.content(
                attachment.title("uploaded-report.pdf"),
                attachment.description("Uploaded · 1.8 MB"),
            ),
            attachment.actions(
                attachment.action(
                    hi("Cancel01Icon"), aria_label="Remove uploaded-report.pdf"
                )
            ),
            state="done",
        ),
        class_name="w-full mx-auto max-w-sm py-12 flex flex-col gap-y-4",
    )

Sizes

Use size to switch between default, sm, and xs.

Default attachmentPDF · 2.4 MB
Small attachmentPDF · 2.4 MB
Extra small attachment
import reflex as rx

from components.icons.hugeicon import hi
from components.ui.attachment import attachment


def attachment_sizes_demo():
    return rx.el.div(
        attachment.root(
            attachment.media(hi("File02Icon")),
            attachment.content(
                attachment.title("Default attachment"),
                attachment.description("PDF · 2.4 MB"),
            ),
            size="default",
        ),
        attachment.root(
            attachment.media(hi("File02Icon")),
            attachment.content(
                attachment.title("Small attachment"),
                attachment.description("PDF · 2.4 MB"),
            ),
            size="sm",
        ),
        attachment.root(
            attachment.media(hi("File02Icon")),
            attachment.content(
                attachment.title("Extra small attachment"),
            ),
            size="xs",
        ),
        class_name="mx-auto w-full max-w-sm py-12 flex flex-col gap-y-4",
    )

Group

Wrap attachments in attachment.group to lay them out in a horizontally scrollable, snapping row with an edge fade.

briefing-notes.pdfPDF · 1.4 MB
workspace.png
workspace.pngPNG · 820 KB
customers.csvCSV · 18 KB
renderer.tsxTSX · 12 KB
import reflex as rx

from components.icons.hugeicon import hi
from components.ui.attachment import attachment

items = [
    {"name": "briefing-notes.pdf", "meta": "PDF · 1.4 MB", "type": "file"},
    {
        "name": "workspace.png",
        "meta": "PNG · 820 KB",
        "src": "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
        "type": "image",
    },
    {"name": "customers.csv", "meta": "CSV · 18 KB", "type": "file"},
    {"name": "renderer.tsx", "meta": "TSX · 12 KB", "type": "file"},
]


def attachment_group_demo():
    return rx.el.div(
        attachment.group(
            rx.foreach(
                items,
                lambda item: attachment.root(
                    rx.cond(
                        item["type"] == "image",
                        attachment.media(
                            rx.el.img(src=item["src"], alt=item["name"]),
                            variant="image",
                        ),
                        attachment.media(hi("File02Icon")),
                    ),
                    attachment.content(
                        attachment.title(item["name"]),
                        attachment.description(item["meta"]),
                    ),
                    attachment.actions(
                        attachment.action(
                            hi("Cancel01Icon"), aria_label=f"Remove {item['name']}"
                        )
                    ),
                    class_name="w-64",
                ),
            ),
            class_name="full",
        ),
        class_name="mx-auto w-full max-w-sm py-12",
    )

Trigger

Add an attachment.trigger to make the whole card open a link or dialog. It fills the card behind the actions, so the actions stay clickable.

research-summary.pdfOpen preview dialog
import reflex as rx

from components.icons.hugeicon import hi
from components.ui.attachment import attachment
from components.ui.dialog import dialog


def attachment_trigger_dialog_demo():
    return rx.el.div(
        dialog.root(
            attachment.root(
                attachment.media(hi("File01Icon")),
                attachment.content(
                    attachment.title("research-summary.pdf"),
                    attachment.description("Open preview dialog"),
                ),
                attachment.actions(
                    attachment.action(hi("Copy01Icon"), aria_label="Copy link"),
                    attachment.action(
                        hi("Cancel01Icon"), aria_label="Remove research-summary.pdf"
                    ),
                ),
                dialog.trigger(attachment.trigger(link=False)),
                class_name="w-full",
            ),
            dialog.portal(
                dialog.backdrop(class_name="backdrop-blur-[3px]"),
                dialog.popup(
                    dialog.title("research-summary.pdf"),
                    dialog.description(
                        "The attachment trigger fills the card and opens the dialog, "
                        "while the actions stay independently clickable above it."
                    ),
                    class_name="max-w-md rounded-2xl",
                ),
            ),
        ),
        class_name="mx-auto w-full max-w-sm py-12",
    )

Accessibility

attachment.action renders a Button, and attachment.trigger renders either a real rx.el.button() or a rx.el.a() if the link prop is set to True. Follow the guidance below so both are operable and announced.

Label icon-only actions

attachment.action is usually icon-only, so give each one an aria-label describing the action and its target.

python

attachment.action(
    hi("Cancel01Icon"), aria_label="Remove market-research.pdf"
)

Label the trigger

attachment.trigger overlays the entire attachment with a clickable surface.

Use aria_label to describe what activating the attachment does. This is required when the trigger has no visible text.

Link trigger (opens a URL)

python

attachment.trigger(
    link=True,
    href=url,
    target="_blank",
    rel="noreferrer",
    aria_label="Open workspace.png",
)

Button trigger (interactive action)

python

attachment.trigger(
    on_click=handle_open,
    aria_label="Open attachment preview",
)

The trigger sits behind the actions in the stacking order, so an attachment.action and the attachment.trigger never trap each other — both remain separately focusable and clickable.

Keyboard scrolling

An attachment.group scrolls horizontally. When its attachments are interactive: a trigger or actions, keyboard users reach off-screen items by tabbing to them. For a row of presentational attachments, make the group itself focusable and scrollable by adding tabIndex={0}, role="group", and an aria-label.

Meaning beyond color

The error state uses a destructive color. Keep the failure reason in attachment.description so the state is not conveyed by color alone.

API Reference

attachment.root

The root attachment container.

PropTypeDefaultDescription
state"idle" | "uploading" | "processing" | "error" | "done""done"The upload state. Drives styling and the shimmer.
size"default" | "sm" | "xs""default"The attachment size.
orientation"horizontal" | "vertical""horizontal"Lay the media beside or above the content.
class_namestring-Additional classes to apply to the root element.

attachment.media

The media slot for an icon or image preview.

PropTypeDefaultDescription
variant"icon" | "image""icon"Whether the media holds an icon or an <img>.
class_namestring-Additional classes to apply to the media slot.

attachment.content

Wraps the title and description.

PropTypeDefaultDescription
class_namestring-Additional classes to apply to the content slot.

attachment.title

The attachment name. Shimmers while the attachment is uploading or processing.

PropTypeDefaultDescription
class_namestring-Additional classes to apply to the title.

attachment.description

Secondary metadata such as the file type, size, or upload status.

PropTypeDefaultDescription
class_namestring-Additional classes to apply to the description.

attachment.actions

A container for one or more actions, aligned to the end of the attachment.

PropTypeDefaultDescription
class_namestring-Additional classes to apply to the actions.

attachment.action

An action button. Renders a Button and accepts all of its props.

PropTypeDefaultDescription
sizeButton["size"]"icon-xs"The button size.
class_namestring-Additional classes to apply to the actions.

attachment.trigger

A full-card overlay that activates the attachment. Renders a rx.el.button by default or a rx.el.a when link=True.

PropTypeDefaultDescription
linkboolFalseIf set, renders an anchor (rx.el.a) instead of a button.
aria_labelstr | NoneNoneAccessibility label for screen readers. Required when no visible text exists.
class_namestr""Additional CSS classes applied to the trigger.

attachment.group

Lays out attachments in a horizontally scrollable, snapping row.

PropTypeDefaultDescription
class_namestring-Additional classes to apply to the group.