Skip to content

Attachment

Preview and download files shared in a conversation. Open the sample attachment, or add your own files and send them with the composer. Files stay in memory and are cleared when this page reloads.

Attach a file to share it in this conversation.

View code
import { useEffect, useRef, useState } from "react";
import { Attachment } from "@stack/ui/attachment";
import { Composer } from "@stack/ui/composer";
import { Message } from "@stack/ui/message";
import { Thread } from "@stack/ui/thread";
type AttachedFile = {
id: string;
src: string;
name: string;
type: string;
};
const sample: AttachedFile = {
id: "sample",
src: "data:text/plain,Example%20attachment%0AReady%20for%20review.",
name: "notes.txt",
type: "text/plain",
};
function FilePreview({
file,
onRemove,
}: {
file: AttachedFile;
onRemove?: () => void;
}) {
return (
<Attachment.Root
src={file.src}
name={file.name}
type={file.type}
onRemove={onRemove}
>
<Attachment.PreviewTrigger />
<Attachment.Remove />
<Attachment.PreviewContent>
<Attachment.Header>
<Attachment.HeaderContent>
<Attachment.Title />
<Attachment.Description />
</Attachment.HeaderContent>
<Attachment.Actions>
<Attachment.Download />
<Attachment.Close />
</Attachment.Actions>
</Attachment.Header>
<Attachment.Content />
</Attachment.PreviewContent>
</Attachment.Root>
);
}
export default function Example() {
const [draft, setDraft] = useState<AttachedFile[]>([]);
const [messages, setMessages] = useState([
{ id: "sample-message", files: [sample] },
]);
const urls = useRef(new Set<string>());
useEffect(() => {
const created = urls.current;
return () => {
for (const url of created) URL.revokeObjectURL(url);
created.clear();
};
}, []);
function attach(files: File[]) {
const added = files.map((file) => {
const src = URL.createObjectURL(file);
urls.current.add(src);
return { id: crypto.randomUUID(), src, name: file.name, type: file.type };
});
setDraft((current) => [...current, ...added]);
}
function submit() {
if (!draft.length) return;
setMessages((current) => [
...current,
{ id: crypto.randomUUID(), files: draft },
]);
setDraft([]);
}
return (
<div className="h-96">
<Thread.Root>
<Thread.Scroller>
<Thread.Messages>
<Thread.Content>
<Thread.Item messageId="prompt">
<Message.Root>
<Message.Content>
<Message.Text
parts={[
{
type: "text",
text: "Attach a file to share it in this conversation.",
},
]}
/>
</Message.Content>
</Message.Root>
</Thread.Item>
{messages.map((message) => (
<Thread.Item key={message.id} messageId={message.id}>
<Message.Root align="end">
<Message.Content>
<div className="flex flex-wrap justify-end gap-2">
{message.files.map((file) => (
<FilePreview key={file.id} file={file} />
))}
</div>
</Message.Content>
</Message.Root>
</Thread.Item>
))}
</Thread.Content>
</Thread.Messages>
<Thread.ScrollButton />
</Thread.Scroller>
<Thread.Footer>
<div className="grid gap-3 pt-4">
{!!draft.length && (
<div
aria-label="Draft attachments"
className="flex flex-wrap gap-2"
>
{draft.map((file) => (
<FilePreview
key={file.id}
file={file}
onRemove={() =>
setDraft((current) =>
current.filter((item) => item.id !== file.id),
)
}
/>
))}
</div>
)}
<Composer.Root
value=""
onChange={() => {}}
onSubmit={submit}
onFiles={attach}
submitState={draft.length ? "ready" : "blocked"}
>
<Composer.AttachMenu onFiles={attach}>
<Composer.AttachFiles />
</Composer.AttachMenu>
<Composer.Footer>
<Composer.Submit label="Send attachments" />
</Composer.Footer>
</Composer.Root>
</div>
</Thread.Footer>
</Thread.Root>
</div>
);
}
import { Attachment } from "@stack/ui/attachment";

Pass a URL, filename, and MIME type. The host application owns URL creation and cleanup for local files.

Tables list component-specific props and inherited control options. Components that forward native props also accept the applicable React HTML attributes and event handlers, including data-* and aria-* attributes.

Also accepts native element props.

Prop Type Required Description
className string No Additional CSS classes for the underlying element.
children ReactNode No Content rendered inside the component.
disabled boolean No Disable interaction.
name string Yes Filename used for display and file-kind detection.
type string No File MIME type.
src string Yes URL of the file or media.
onRemove (() => void) No Remove this attachment from the host’s state.
Prop Type Required Description
children ReactNode Yes Content rendered inside the component.
src string Yes URL of the file or media.
name string Yes Filename used for display and file-kind detection.
type string No File MIME type. Default: "".
onRemove (() => void) No Remove this attachment from the host’s state.
disabled boolean No Disable interaction. Default: false.

Also accepts native element props.

Prop Type Required Description
handle Handle<unknown> No A handle to associate the trigger with a dialog.
payload unknown No A payload to pass to the dialog when it is opened.
id string No Element identifier.
nativeButton boolean No Whether the component renders a native <button> element when replacing it via the render prop.
children ReactNode No Content rendered inside the component.
className string | ((state: DialogTriggerState) => string | undefined) No Additional CSS classes for the underlying element.
render ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogTriggerState> No Replace the underlying element using render composition.
style CSSProperties | ((state: DialogTriggerState) => CSSProperties | undefined) No Inline styles for the underlying element.

This component takes no props.

Also accepts native element props.

Prop Type Required Description
style CSSProperties | ((state: AlertDialogPopupState) => CSSProperties | undefined) No Inline styles for the underlying element.
className string | ((state: AlertDialogPopupState) => string | undefined) No Additional CSS classes for the underlying element.
children ReactNode No Content rendered inside the component.
initialFocus boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void) No Determines the element to focus when the dialog is opened.
finalFocus boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) No Determines the element to focus when the dialog is closed.
render ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, AlertDialogPopupState> No Replace the underlying element using render composition.

Also accepts native element props.

Prop Type Required Description
className string No Additional CSS classes for the underlying element.
children ReactNode No Content rendered inside the component.
size "default" | "sm" No Size variant. Default: "default".

Also accepts native element props.

Prop Type Required Description
className string No Additional CSS classes for the underlying element.
children ReactNode No Content rendered inside the component.

Also accepts native element props.

Prop Type Required Description
className string No Additional CSS classes for the underlying element.
children ReactNode No Content rendered inside the component.

Also accepts native element props.

Prop Type Required Description
children ReactNode No Content rendered inside the component.
className string | ((state: AlertDialogTitleState) => string | undefined) No Additional CSS classes for the underlying element.
render ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, AlertDialogTitleState> No Replace the underlying element using render composition.
style CSSProperties | ((state: AlertDialogTitleState) => CSSProperties | undefined) No Inline styles for the underlying element.

Also accepts native element props.

Prop Type Required Description
children ReactNode No Content rendered inside the component. Default: "Attachment preview".
className string | ((state: AlertDialogDescriptionState) => string | undefined) No Additional CSS classes for the underlying element.
render ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, AlertDialogDescriptionState> No Replace the underlying element using render composition.
style CSSProperties | ((state: AlertDialogDescriptionState) => CSSProperties | undefined) No Inline styles for the underlying element.

This component takes no props.

Also accepts native element props.

Prop Type Required Description
style CSSProperties | ((state: ButtonState) => CSSProperties | undefined) No Inline styles for the underlying element.
className string | ((state: ButtonState) => string | undefined) No Additional CSS classes for the underlying element.
variant "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null No Appearance variant.
size "default" | "sm" | "xs" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null No Size variant.
focusableWhenDisabled boolean No Whether the button should be focusable when disabled.

Also accepts native element props.

Prop Type Required Description
style CSSProperties | ((state: AlertDialogCloseState) => CSSProperties | undefined) No Inline styles for the underlying element.
className string | ((state: AlertDialogCloseState) => string | undefined) No Additional CSS classes for the underlying element.
nativeButton boolean No Whether the component renders a native <button> element when replacing it via the render prop.

Also accepts native element props.

Prop Type Required Description
style CSSProperties | ((state: ButtonState) => CSSProperties | undefined) No Inline styles for the underlying element.
className string | ((state: ButtonState) => string | undefined) No Additional CSS classes for the underlying element.
variant "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null No Appearance variant.
size "default" | "sm" | "xs" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null No Size variant.
render ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState> No Replace the underlying element using render composition.
focusableWhenDisabled boolean No Whether the button should be focusable when disabled.
nativeButton boolean No Whether the component renders a native <button> element when replacing it via the render prop.