Skip to content
goodtabs UI
BrowseBrowse components

File list

Files as rows with type icon, name, size and one download action.

npx shadcn@latest add https://ui.goodtabs.co/r/file-list.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/file-list

  • brand-guidelines.pdf

    PDF · 2.4 MB

  • tokens.json

    JSON · 8.2 kB

Icon tile, name, type and size, then the action.

Mixed types

  • design-handoff-v3.pdf

    PDF · 4.8 MB

  • icons.zip

    ZIP · 126 MB

  • hero-dark.png

    PNG · 612 kB

  • button.tsx

    TSX · 940 B

  • release-notes.md

    MD · 3.1 kB

Archive, image, code and text pick their own icon.

Long names

  • quarterly-design-system-audit-final-final-reviewed-by-everyone.pdf

    PDF · 19 MB

  • LICENSE

    FILE · 1.1 kB

Names truncate; the action and size never move.

Usage

import { FileList } from "@/components/ui/file-list"

<FileList files={[{ name: "spec.pdf", size: 240000, href: "/spec.pdf" }]} />

Props

PropTypeDefaultDescription
files{ name: string; size?: number; href: string }[]Name with extension, size in bytes when known, and the download link.
labelstring"Files"Accessible name for the list.

Source

file-list/file-list.tsx
import * as React from "react";
import { DownloadIcon, FileIcon, FileTextIcon, FileArchiveIcon, ImageIcon, FileCodeIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

type FileItem = {
  /** File name with its extension. */
  name: string;
  /** Size in bytes. Left out, the row shows the type alone. */
  size?: number;
  /** Where the action points. */
  href: string;
};

type FileListProps = Omit<React.ComponentProps<"ul">, "children"> & {
  files: FileItem[];
  /** Accessible name for the list. */
  label?: string;
};

const ICONS: Record<string, React.ComponentType<React.ComponentProps<"svg">>> = {
  pdf: FileTextIcon,
  md: FileTextIcon,
  txt: FileTextIcon,
  png: ImageIcon,
  jpg: ImageIcon,
  svg: ImageIcon,
  zip: FileArchiveIcon,
  json: FileCodeIcon,
  tsx: FileCodeIcon,
};

/** 1 kB is 1000 bytes. One decimal under 10, none above. Empty when there is no size to show. */
export function formatBytes(bytes?: number) {
  if (bytes === undefined || !Number.isFinite(bytes)) return "";
  if (bytes < 1000) return `${bytes} B`;
  const units = ["kB", "MB", "GB", "TB"];
  let value = bytes;
  let i = -1;
  do {
    value /= 1000;
    i += 1;
  } while (Math.round(value) >= 1000 && i < units.length - 1);
  return `${value < 9.95 ? value.toFixed(1) : Math.round(value)} ${units[i]}`;
}

function extensionOf(name: string) {
  const dot = name.lastIndexOf(".");
  return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
}

/**
 * Files as rows: type icon, name, extension and size in mono, and one download action.
 * Rows share hairlines inside one bordered box.
 */
export function FileList({ files, label = "Files", className, ...props }: FileListProps) {
  return (
    <ul
      aria-label={label}
      className={cn(
        "m-0 w-full list-none divide-y divide-border rounded-lg border border-border bg-background p-0",
        className,
      )}
      {...props}
    >
      {files.map((file, i) => {
        const ext = extensionOf(file.name);
        const size = formatBytes(file.size);
        const Icon = ICONS[ext] ?? FileIcon;
        return (
          <li key={i} className="flex items-center gap-3 px-3 py-2">
            <span
              aria-hidden="true"
              className="inline-grid size-8 shrink-0 place-items-center rounded-md bg-muted text-foreground"
            >
              <Icon className="size-4" />
            </span>
            <div className="min-w-0 flex-1">
              <p className="m-0 truncate text-sm text-foreground">{file.name}</p>
              <p className="m-0 font-mono text-xs text-muted-foreground tabular-nums">
                {ext ? ext.toUpperCase() : "FILE"}
                {size && " · "}
                {size}
              </p>
            </div>
            <Button asChild variant="ghost" size="icon-sm" className="rounded-full">
              <a href={file.href} download aria-label={`Download ${file.name}`}>
                <DownloadIcon />
              </a>
            </Button>
          </li>
        );
      })}
    </ul>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/file-list/prompt.md, or the Copy AI prompt button above.

When to use

  • Attachments on a message, ticket or release
  • A small set of downloads on a docs or handoff page

When not to use

  • Picking or uploading files: use file-drop
  • Hundreds of files with sorting: use a table

Agent notes

  • One action per row by design: a download link with an icon-only button look and an aria-label
  • Size is bytes in, decimal units out (1 kB is 1000 bytes); with no size the row shows the type alone
  • The icon comes from the extension; unknown extensions get a plain file icon
  • Server component; the link is a real anchor with the download attribute