FileAttachment

Render a file/image chip. Use isImage + url for thumbnails, display="image-only" for previews, and onRemove to show the close control.

report.pdf22.5 KB
preview.png
preview.png117.2 KB
preview.png
Getting Started
$pnpm dlx shadcn@latest add https://agent-elements.21st.dev/r/file-attachment.json
Examples
File + image
report.pdf22.5 KB
design.png
design.png117.2 KB
Image only
hero.png
Removable file
notes.md4.1 KB
Source

Copy and paste the following code into your project, or run the install command above to pull it in automatically.

components/input/file-attachment.tsx
import { useState } from "react";
import {
  IconX as X,
  IconFileText as FileText,
  IconFileCode as FileCode,
  IconFileTypeJs as FileJson,
  IconPhoto as ImageIcon,
} from "@tabler/icons-react";
import { cn } from "../../utils/cn";

export type FileAttachmentProps = {
  id: string;
  filename: string;
  size?: number;
  isImage?: boolean;
  url?: string;
  onRemove?: () => void;
  className?: string;
  display?: "chip" | "image-only";
};

function formatFileSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

type FileIconName = "image" | "code" | "data" | "text";

function getFileIconName(filename: string, isImage?: boolean): FileIconName {
  if (isImage) return "image";

  const ext = filename.split(".").pop()?.toLowerCase();

  if (
    [
      "js",
      "ts",
      "jsx",
      "tsx",
      "py",
      "rb",
      "go",
      "rs",
      "java",
      "kt",
      "swift",
      "c",
      "cpp",
      "h",
      "hpp",
      "cs",
      "php",
    ].includes(ext || "")
  ) {
    return "code";
  }

  if (["json", "yaml", "yml", "xml"].includes(ext || "")) {
    return "data";
  }

  return "text";
}

function renderFileIcon(iconName: FileIconName) {
  switch (iconName) {
    case "image":
      return <ImageIcon className="size-4 text-muted-foreground" />;
    case "code":
      return <FileCode className="size-4 text-muted-foreground" />;
    case "data":
      return <FileJson className="size-4 text-muted-foreground" />;
    default:
      return <FileText className="size-4 text-muted-foreground" />;
  }
}

export function FileAttachment({
  filename,
  size,
  isImage,
  url,
  onRemove,
  className,
  display = "chip",
}: FileAttachmentProps) {
  const [isHovered, setIsHovered] = useState(false);
  const iconName = getFileIconName(filename, isImage);
  const isImageOnly = display === "image-only" && isImage && !!url;

  return (
    <div
      className={cn(
        "relative bg-muted/50 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding))]",
        isImageOnly
          ? "size-10 flex items-center justify-center"
          : "flex items-center gap-2 pl-1 pr-2 py-1 min-w-[120px] max-w-[200px]",
        className,
      )}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      {isImageOnly ? (
        <div className="size-8 overflow-hidden shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]">
          <img
            src={url}
            alt={filename}
            className="w-full h-full object-cover"
          />
        </div>
      ) : (
        <>
          {isImage && url ? (
            <div className="w-8 self-stretch overflow-hidden shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]">
              <img
                src={url}
                alt={filename}
                className="w-full h-full object-cover aspect-square"
              />
            </div>
          ) : (
            <div className="flex items-center justify-center w-8 self-stretch bg-muted shrink-0 rounded-[calc(var(--an-input-border-radius)-var(--an-context-padding)-2px)]">
              {renderFileIcon(iconName)}
            </div>
          )}

          <div className="flex flex-col min-w-0">
            <span
              className="text-sm font-medium text-foreground truncate"
              title={filename}
            >
              {filename}
            </span>
            {size !== undefined && (
              <span className="text-[10px] text-muted-foreground">
                {formatFileSize(size)}
              </span>
            )}
          </div>
        </>
      )}

      {onRemove && (
        <button
          onClick={(e) => {
            e.stopPropagation();
            onRemove();
          }}
          className={`absolute -top-1.5 -right-1.5 size-4 rounded-full bg-background border border-border
                     flex items-center justify-center transition-[opacity,transform] duration-150 ease-out active:scale-[0.97] z-10
                     text-muted-foreground hover:text-foreground
                     ${isHovered ? "opacity-100" : "opacity-0"}`}
          type="button"
        >
          <X className="size-3" />
        </button>
      )}
    </div>
  );
}
API reference
Prop
Type
Required
id
string
Yes
filename
string
Yes
size
number
No
isImage
boolean
No
url
string
No
onRemove
() => void
No
className
string
No
display
"chip" | "image-only"
No