Attachment

A file inside a message, with its preview, its metadata, its upload state and its own actions.

avatar.png PNG · 84 KB
report.pdf Uploading · 2.1 MB

Installation

uvx django_shadcn@latest add attachment

Usage

<c-attachment>
    <c-attachment.media variant="image">
        <img src="{{ file.url }}" alt="">
    </c-attachment.media>
    <c-attachment.content>
        <c-attachment.title>{{ file.name }}</c-attachment.title>
        <c-attachment.description>{{ file.type }} · {{ file.size }}</c-attachment.description>
    </c-attachment.content>
    <c-attachment.actions>
        <c-attachment.action aria-label="Remove">
            <c-icon name="x" />
        </c-attachment.action>
    </c-attachment.actions>
</c-attachment>

The card is presentational. Nothing here uploads a file or tracks progress — you set state from whatever already knows, and the card follows.

States

Five, and each one changes more than a label: idle dashes the border, uploading and processing shimmer the title, error turns the border and the media red.

Drop a file here Nothing chosen yet
report.pdf Uploading · 2.1 MB
recording.wav Transcribing
archive.zip Too large, 40 MB over the limit
notes.md Markdown · 4 KB
<c-attachment state="uploading">...</c-attachment>
<c-attachment state="error">...</c-attachment>

The shimmer on the title is the shimmer utility from input.css. It stops by itself when the reader has asked for reduced motion.

Sizes and orientation

size takes default, sm and xs. orientation="vertical" turns the card into a tile, with the media on top and the actions floating over its corner.

small.txt Text · 1 KB
tiny.txt
avatar.png 84 KB
<c-attachment size="sm">...</c-attachment>
<c-attachment size="xs">...</c-attachment>
<c-attachment orientation="vertical">...</c-attachment>

Examples

A card that opens the file

trigger covers the whole card, so the card itself is the target. It sits behind the actions on purpose: the remove button keeps its own click and its own tab stop, and neither swallows the other.

avatar.png PNG · 84 KB
<c-attachment>
    ...
    <c-attachment.actions>
        <c-attachment.action aria-label="Remove">
            <c-icon name="x" />
        </c-attachment.action>
    </c-attachment.actions>
    <c-attachment.trigger tag="a" href="{{ file.url }}" aria-label="Open {{ file.name }}"></c-attachment.trigger>
</c-attachment>

The trigger has no text of its own, so it needs an aria-label. Without one it is an unlabelled control covering the entire card.

Leave tag off and it renders a button with type="button", for a card that opens a preview instead of navigating.

A row of files

group scrolls sideways, snaps each card into place and fades the edge you have scrolled away from.

one.png
two.png
three.png
four.png
<c-attachment.group>
    <c-attachment orientation="vertical">...</c-attachment>
    <c-attachment orientation="vertical">...</c-attachment>
</c-attachment.group>

The scroll, the snapping and the fade are all CSS. There is no JavaScript in this component.

Inside a message

avatar.png PNG · 84 KB
Here is the file you asked for.