Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new
DocumentationAttachment

Attachment

File attachments with media, actions and upload states. AttachmentTrigger defaults to a non-submit native button. An explicit type and composed child behavior are preserved.

Interactive preview

Rendered from the shared Frontfriend stories with React Base, the same preview Studio shows. Styling comes from your Design System; the code below is native to Radix UI.

Installation

Complete project setup first. Your configuration must select react/radix and include your Design System ID.

pnpm exec frontfriend download attachment -f react --primitive radix

Review dependencies reported by the downloader and keep your source changes in version control.

Usage

import {
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
} from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
<Attachment
  orientation="horizontal"
  state="done"
  className="w-72"
>
  <AttachmentMedia>
    <Icon name="FileText" />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>report.pdf</AttachmentTitle>
    <AttachmentDescription>PDF · 12 KB</AttachmentDescription>
  </AttachmentContent>
</Attachment>;

Examples

Inside Form

import {
  Attachment,
  AttachmentTrigger,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
} from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { Button } from "@/components/ui/button";
import * as React from "react";
export default function AttachmentExample() {
  const [opened, setOpened] = React.useState(0);
  const [submitted, setSubmitted] = React.useState(0);
  return (
    <form
      className="flex w-[360px] max-w-[calc(100vw-2rem)] flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault();
        setSubmitted((count) => count + 1);
      }}
    >
      <Attachment>
        <AttachmentTrigger
          aria-label="Open report.pdf"
          onClick={() => setOpened((count) => count + 1)}
        />
        <AttachmentMedia>
          <Icon name="FileText" size="4" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>report.pdf</AttachmentTitle>
          <AttachmentDescription>Open attachment</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Button type="submit" variant="outline">
        Submit form
      </Button>
      <output className="text-sm text-neutral-mid">
        Opened: {opened}. Submitted: {submitted}.
      </output>
    </form>
  );
}

Default

import {
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
  AttachmentActions,
  AttachmentAction,
} from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
<div className="flex w-full max-w-sm flex-col gap-3">
  <Attachment className="w-full">
    <AttachmentMedia>
      <Icon name="FileCode" size="4" />
    </AttachmentMedia>
    <AttachmentContent>
      <AttachmentTitle>message-renderer.tsx</AttachmentTitle>
      <AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
    </AttachmentContent>
    <AttachmentActions>
      <AttachmentAction aria-label="Remove message-renderer.tsx">
        <Icon name="X" size="4" />
      </AttachmentAction>
    </AttachmentActions>
  </Attachment>
</div>;

Image Group

import {
  AttachmentGroup,
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
} from "@/components/ui/attachment";
const images = [
  {
    name: "workspace.png",
    meta: "PNG · 820 KB",
    src: "https://github.com/shadcn.png",
    alt: "Workspace",
  },
  {
    name: "desk-reference.jpg",
    meta: "JPG · 1.1 MB",
    src: "https://github.com/vercel.png",
    alt: "Desk",
  },
];
<div className="flex w-full max-w-sm flex-col gap-3">
  <AttachmentGroup>
    {images.map((image) => (
      <Attachment key={image.name} orientation="vertical">
        <AttachmentMedia variant="image">
          <img src={image.src} alt={image.alt} />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>{image.name}</AttachmentTitle>
          <AttachmentDescription>{image.meta}</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    ))}
  </AttachmentGroup>
</div>;

Uploading

import {
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
  AttachmentActions,
  AttachmentAction,
} from "@/components/ui/attachment";
import { Spinner } from "@/components/ui/spinner";
import { Icon } from "@/components/ui/icon";
<div className="flex w-full max-w-sm flex-col gap-3">
  <Attachment state="uploading" className="w-full">
    <AttachmentMedia>
      <Spinner />
    </AttachmentMedia>
    <AttachmentContent>
      <AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
      <AttachmentDescription>Uploading · 64%</AttachmentDescription>
    </AttachmentContent>
    <AttachmentActions>
      <AttachmentAction aria-label="Cancel upload">
        <Icon name="X" size="4" />
      </AttachmentAction>
    </AttachmentActions>
  </Attachment>
</div>;

States

import {
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
} from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
<div className="flex w-full max-w-sm flex-col gap-3">
  {(["idle", "uploading", "processing", "error", "done"] as const).map(
    (state) => (
      <Attachment key={state} state={state} className="w-full">
        <AttachmentMedia>
          <Icon name="FileText" size="4" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>report.pdf</AttachmentTitle>
          <AttachmentDescription>{state}</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    ),
  )}
</div>;

API reference

Frontfriend-owned props. Native HTML and inherited primitive props remain documented by Radix UI.

Attachment

Prop Type Default Description
orientation unknown "horizontal"
size unknown "default"
state "idle" | "uploading" | "processing" | "error" | "done" "done"

AttachmentMedia

Prop Type Default Description
variant unknown "icon"

AttachmentTrigger

Prop Type Default Description
asChild boolean false

Configuration determines the actual classes and available styling hooks. See Theming & component configuration before introducing a repeated local override.