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 Reka UI.

Installation

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

pnpm exec frontfriend download attachment -f vue --primitive reka

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

Usage

<script setup lang="ts">
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentDescription } from "@/components/ui/attachment";

const args = {
  "orientation": "horizontal",
  "state": "done",
  "title": "report.pdf",
  "description": "PDF · 12 KB",
} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown> & {
    orientation?: "horizontal" | "vertical";
    state?: "idle" | "uploading" | "processing" | "error" | "done";
  };
</script>

<template>
  <Attachment :orientation="args.orientation" :state="args.state"
    ><AttachmentMedia><Icon name="FileText" /></AttachmentMedia
    ><AttachmentContent
      ><AttachmentTitle>{{ args.title }}</AttachmentTitle
      ><AttachmentDescription>{{
        args.description
      }}</AttachmentDescription></AttachmentContent
    ></Attachment
  >
</template>

Examples

Inside Form

<script setup lang="ts">
import { ref } from "vue";
import { Attachment } from "@/components/ui/attachment";
import { AttachmentTrigger } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentDescription } from "@/components/ui/attachment";
import { Button } from "@/components/ui/button";

const args = {

} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown>;
const opened = ref(0);
const submitted = ref(0);
</script>

<template>
  <form
    class="flex w-[360px] max-w-[calc(100vw-2rem)] flex-col gap-3"
    @submit.prevent="submitted += 1"
  >
    <Attachment>
      <AttachmentTrigger aria-label="Open report.pdf" @click="opened += 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 class="text-sm text-neutral-mid"
      >Opened: {{ opened }}. Submitted: {{ submitted }}.</output
    >
  </form>
</template>

Default

<script setup lang="ts">
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentDescription } from "@/components/ui/attachment";
import { AttachmentActions } from "@/components/ui/attachment";
import { AttachmentAction } from "@/components/ui/attachment";

const args = {

} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown>;
</script>

<template>
  <div class="flex w-full max-w-sm flex-col gap-3">
    <Attachment class="w-full">
      <AttachmentMedia>
        <Icon name="FileCode" size="4" />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>message-renderer.vue</AttachmentTitle>
        <AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label="Remove message-renderer.vue">
          <Icon name="X" size="4" />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  </div>
</template>

Image Group

<script setup lang="ts">
import { AttachmentGroup } from "@/components/ui/attachment";
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { 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",
  },
];

const args = {

} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown>;
</script>

<template>
  <div class="flex w-full max-w-sm flex-col gap-3">
    <AttachmentGroup>
      <Attachment
        v-for="image in images"
        :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>
</template>

Uploading

<script setup lang="ts">
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Spinner } from "@/components/ui/spinner";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentDescription } from "@/components/ui/attachment";
import { AttachmentActions } from "@/components/ui/attachment";
import { AttachmentAction } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";

const args = {

} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown>;
</script>

<template>
  <div class="flex w-full max-w-sm flex-col gap-3">
    <Attachment state="uploading" class="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>
</template>

States

<script setup lang="ts">
import { Attachment } from "@/components/ui/attachment";
import { AttachmentMedia } from "@/components/ui/attachment";
import { Icon } from "@/components/ui/icon";
import { AttachmentContent } from "@/components/ui/attachment";
import { AttachmentTitle } from "@/components/ui/attachment";
import { AttachmentDescription } from "@/components/ui/attachment";

const args = {

} satisfies Partial<
  InstanceType<typeof Attachment> extends { $props: infer Props } ? Props : {}
> &
  Record<string, unknown>;
const states = ["idle", "uploading", "processing", "error", "done"] as const;
</script>

<template>
  <div class="flex w-full max-w-sm flex-col gap-3">
    <Attachment
      v-for="state in states"
      :key="state"
      :state="state"
      class="w-full"
    >
      <AttachmentMedia>
        <Icon name="FileText" size="4" />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>report.pdf</AttachmentTitle>
        <AttachmentDescription>{{ state }}</AttachmentDescription>
      </AttachmentContent>
    </Attachment>
  </div>
</template>

API reference

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

Attachment

Prop Type Default Description
class HTMLAttributes["class"] —
orientation AttachmentVariants["orientation"] —
size AttachmentVariants["size"] —
state "idle" | "uploading" | "processing" | "error" | "done" —

AttachmentAction

Prop Type Default Description
class HTMLAttributes["class"] —
size ButtonVariants["size"] —
variant ButtonVariants["variant"] —

AttachmentActions

Prop Type Default Description
class HTMLAttributes["class"] —

AttachmentContent

Prop Type Default Description
class HTMLAttributes["class"] —

AttachmentDescription

Prop Type Default Description
class HTMLAttributes["class"] —

AttachmentGroup

Prop Type Default Description
class HTMLAttributes["class"] —

AttachmentMedia

Prop Type Default Description
class HTMLAttributes["class"] —
variant AttachmentMediaVariants["variant"] —

AttachmentTitle

Prop Type Default Description
class HTMLAttributes["class"] —

AttachmentTrigger

Prop Type Default Description
class HTMLAttributes["class"] —
type ButtonHTMLAttributes["type"] —

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