Skip to content
oks-ui

App screens

File manager

A file-manager home screen: six quick-action tiles (Create, Upload, Create Folder, Edit PDF, Get Signatures, Sign Yourself), a Suggested Folders grid, and a Suggested Files section that toggles between a card grid and a compact list — each file or folder carrying its own Dropdown menu.

Built withButtonDropdown
file-manager.tsx

How it's built

1

The action tiles are real Buttons with their content reflowed to a vertical layout via className — variant/color still come from the real component, not a hand-rolled one.

2

Grid vs. list is one `view` state driving two different layouts over the same `FILES` array — no duplicated data.

3

Each file/folder card's `⋮` is a Dropdown with an accessibly-named icon trigger, exactly like every other per-row menu on this site.

Source

One file · copy & paste
file-manager.tsx
"use client";

import { useState } from "react";
import { Button } from "oks-ui/button";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import "oks-ui/button.css";
import "oks-ui/dropdown.css";
import { GridIcon, MoreIcon, PlusIcon } from "../../showcase/icons";

// An upward arrow into a tray — "upload".
function UploadIcon({ size = 20 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M10 12.5V3.5M6.5 7l3.5-3.5L13.5 7" />
      <path d="M3.5 13v2a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5v-2" />
    </svg>
  );
}

// A folder with a plus badge — "create folder".
function FolderPlusIcon({ size = 20 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M2.5 5.5a1 1 0 0 1 1-1h3.8l1.5 1.8h7.2a1 1 0 0 1 1 1v7.2a1 1 0 0 1-1 1h-12.5a1 1 0 0 1-1-1Z" />
      <path d="M10 9v4M8 11h4" />
    </svg>
  );
}

// A pencil over a rectangle — "edit a document".
function EditDocIcon({ size = 20 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M12 3.5 16.5 8 8 16.5H3.5V12Z" />
    </svg>
  );
}

// A flowing signature line — "get signatures".
function SignatureIcon({ size = 20 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M2.5 14c1.5-1 2.5-4 3.5-4s.5 3 2 3 2-5 3.5-5 1 4 2.5 4 1.5-2.5 3.5-2.5" />
      <path d="M3 17h14" />
    </svg>
  );
}

// A pen writing on a line — "sign yourself".
function SignPenIcon({ size = 20 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M11 3.5 16.5 9 7 18.5H2.5V14Z" />
      <path d="M2.5 18.5h4" />
    </svg>
  );
}

// A folder, filled — used for folder cards.
function FolderIcon({ size = 20, className }: { size?: number; className?: string }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="currentColor" className={className} aria-hidden="true">
      <path d="M2 5a1 1 0 0 1 1-1h4.5l1.8 2h7.7a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z" />
    </svg>
  );
}

// Three horizontal lines — the "list view" half of a grid/list toggle.
function ListViewIcon({ size = 16 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" aria-hidden="true">
      <path d="M3 4h10M3 8h10M3 12h10" />
    </svg>
  );
}

// A circled "i" — "details".
function InfoIcon({ size = 16 }: { size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.5" />
      <path d="M8 7.2v4M8 5.2v.1" />
    </svg>
  );
}

const TOOLS = [
  { key: "create", label: "Create", icon: <PlusIcon size={20} />, primary: true },
  { key: "upload", label: "Upload or Drop", icon: <UploadIcon /> },
  { key: "folder", label: "Create Folder", icon: <FolderPlusIcon /> },
  { key: "edit", label: "Edit PDF", icon: <EditDocIcon /> },
  { key: "sig", label: "Get Signatures", icon: <SignatureIcon /> },
  { key: "sign", label: "Sign Yourself", icon: <SignPenIcon /> },
];

const OWNER = "Alex Rivera";

const FOLDERS = ["Tech Innovations", "Digital Masterpieces", "Software Solutions", "Code Inspirations"];

type DocVariant = "invoice" | "resume" | "letter" | "photo";

type FileItem = { id: number; name: string; variant: DocVariant };

const FILES: FileItem[] = [
  { id: 1, name: "2024 Product Overview", variant: "invoice" },
  { id: 2, name: "Annual Review", variant: "resume" },
  { id: 3, name: "Trends Development", variant: "letter" },
  { id: 4, name: "Performance Analysis", variant: "letter" },
  { id: 5, name: "Case Study", variant: "resume" },
  { id: 6, name: "Effectiveness Report", variant: "letter" },
  { id: 7, name: "Landscape Overview", variant: "letter" },
  { id: 8, name: "Brunch Recipe", variant: "photo" },
];

function DocThumb({ variant }: { variant: DocVariant }) {
  if (variant === "photo") {
    return (
      <div
        className="h-full w-full"
        style={{ background: "linear-gradient(140deg, #e0623a 0%, #7a2d2d 55%, #1c1420 100%)" }}
      />
    );
  }
  if (variant === "invoice") {
    return (
      <div className="flex h-full w-full flex-col gap-1.5 p-3">
        <div className="flex items-center justify-between">
          <span className="h-1.5 w-8 rounded-full bg-ink-faint/60" />
          <span className="h-1.5 w-10 rounded-full bg-[color:var(--oks-color-success-400)]" />
        </div>
        <span className="mt-1 h-1 w-14 rounded-full bg-ink-faint/40" />
        <span className="h-1 w-16 rounded-full bg-ink-faint/40" />
        <div className="mt-2 flex flex-col gap-1 border-t border-line pt-2">
          {Array.from({ length: 3 }, (_, i) => (
            <div key={i} className="flex justify-between">
              <span className="h-1 w-10 rounded-full bg-ink-faint/40" />
              <span className="h-1 w-6 rounded-full bg-ink-faint/40" />
            </div>
          ))}
        </div>
      </div>
    );
  }
  if (variant === "resume") {
    return (
      <div className="flex h-full w-full flex-col gap-2 p-3">
        <span className="h-2 w-16 rounded-full bg-ink-faint/60" />
        <span className="h-1 w-10 rounded-full bg-[color:var(--oks-color-danger-400)]" />
        <div className="mt-2 flex flex-col gap-1">
          <span className="h-1 w-8 rounded-full bg-ink-faint/40" />
          <span className="h-1 w-full rounded-full bg-ink-faint/30" />
          <span className="h-1 w-full rounded-full bg-ink-faint/30" />
        </div>
        <div className="mt-2 flex flex-col gap-1">
          <span className="h-1 w-8 rounded-full bg-ink-faint/40" />
          <span className="h-1 w-full rounded-full bg-ink-faint/30" />
        </div>
      </div>
    );
  }
  return (
    <div className="flex h-full w-full flex-col gap-1.5 p-3">
      <span className="h-1 w-16 self-end rounded-full bg-[color:var(--oks-color-info-400)]" />
      <span className="mt-2 h-1 w-full rounded-full bg-ink-faint/30" />
      <span className="h-1 w-full rounded-full bg-ink-faint/30" />
      <span className="h-1 w-3/4 rounded-full bg-ink-faint/30" />
      <span className="mt-2 h-1 w-full rounded-full bg-ink-faint/30" />
      <span className="h-1 w-full rounded-full bg-ink-faint/30" />
    </div>
  );
}

function CardMenu({ label }: { label: string }) {
  return (
    <Dropdown placement="bottom-end">
      <DropdownTrigger>
        <Button isIconOnly aria-label={label} variant="ghost" size="xs">
          <MoreIcon size={16} />
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label={label}>
        <DropdownItem key="rename">Rename</DropdownItem>
        <DropdownItem key="share">Share</DropdownItem>
        <DropdownItem key="delete" classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
          Delete
        </DropdownItem>
      </DropdownMenu>
    </Dropdown>
  );
}

export function FileManager() {
  const [view, setView] = useState<"grid" | "list">("grid");

  return (
    // @container has to live on its OWN wrapper, not an element that also
    // uses @xl: utilities conditioned by it — an element can't query its
    // own container for its own styling (circular per the CSS Containment
    // spec), so those utilities would silently be evaluated against the
    // wrong (ancestor) container and never apply.
    <div className="@container w-full">
      <div className="flex w-full flex-col gap-6 bg-paper p-6">
      <div className="grid grid-cols-3 gap-3 @xl:grid-cols-6">
        {TOOLS.map((t) => (
          <Button
            key={t.key}
            variant={t.primary ? "solid" : "bordered"}
            className={`h-auto flex-col items-start gap-3 rounded-[var(--r-lg)] p-4 text-left ${
              t.primary ? "bg-ink text-paper hover:opacity-90" : ""
            }`}
          >
            {t.icon}
            <span className="text-[0.85rem] font-medium">{t.label}</span>
          </Button>
        ))}
      </div>

      <div className="flex flex-col gap-3">
        <div className="flex items-center justify-between">
          {/* h2, not h3: this preview's own outline starts fresh at the
              pattern page's h1 with nothing in between, so h3 here would
              skip a level (axe's heading-order). */}
          <h2 className="text-[1.05rem] font-bold text-ink">Suggested Folders</h2>
          <Button variant="bordered" size="sm" radius="full">
            View more
          </Button>
        </div>
        <div className="grid grid-cols-2 gap-4 @xl:grid-cols-4">
          {FOLDERS.map((name) => (
            <div key={name} className="flex flex-col gap-2">
              <div className="flex aspect-[4/3] items-center justify-center rounded-[var(--r-lg)] bg-paper-sunken">
                <FolderIcon size={56} className="text-[color:var(--oks-color-info-400)]" />
              </div>
              <div className="flex items-center gap-2">
                <FolderIcon size={16} className="shrink-0 text-[color:var(--oks-color-info-500)]" />
                <div className="min-w-0 flex-1">
                  <p className="truncate text-[0.85rem] font-medium text-ink">{name}</p>
                  <p className="truncate text-[0.75rem] text-ink-faint">Folder · {OWNER}</p>
                </div>
                <CardMenu label={`${name} options`} />
              </div>
            </div>
          ))}
        </div>
      </div>

      <div className="flex flex-col gap-3">
        <div className="flex items-center justify-between">
          <h2 className="text-[1.05rem] font-bold text-ink">Suggested Files</h2>
          <div className="flex items-center gap-1">
            <Button isIconOnly aria-label="List view" variant={view === "list" ? "soft" : "ghost"} color={view === "list" ? "primary" : "default"} size="sm" onClick={() => setView("list")}>
              <ListViewIcon />
            </Button>
            <Button isIconOnly aria-label="Grid view" variant={view === "grid" ? "soft" : "ghost"} color={view === "grid" ? "primary" : "default"} size="sm" onClick={() => setView("grid")}>
              <GridIcon size={16} />
            </Button>
            <Button isIconOnly aria-label="Show details" variant="ghost" size="sm">
              <InfoIcon />
            </Button>
          </div>
        </div>

        {view === "grid" ? (
          <div className="grid grid-cols-2 gap-4 @xl:grid-cols-4">
            {FILES.map((f) => (
              <div key={f.id} className="flex flex-col gap-2">
                <div className="relative aspect-[4/3] overflow-hidden rounded-[var(--r-lg)] border border-line bg-paper-sunken">
                  <DocThumb variant={f.variant} />
                  <span className="absolute bottom-2 left-2 rounded-[var(--r-xs,0.25rem)] bg-[color:var(--oks-color-danger-500)] px-1.5 py-0.5 text-[0.65rem] font-bold text-white">
                    PDF
                  </span>
                </div>
                <div className="flex items-center gap-2">
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-[0.85rem] font-medium text-ink">{f.name}</p>
                    <p className="truncate text-[0.75rem] text-ink-faint">PDF · {OWNER}</p>
                  </div>
                  <CardMenu label={`${f.name} options`} />
                </div>
              </div>
            ))}
          </div>
        ) : (
          <div className="flex flex-col divide-y divide-line">
            {FILES.map((f) => (
              <div key={f.id} className="flex items-center gap-3 py-2.5">
                <span className="flex h-8 w-6 shrink-0 items-center justify-center rounded-[var(--r-xs,0.25rem)] bg-[color:var(--oks-color-danger-500)] text-[0.55rem] font-bold text-white">
                  PDF
                </span>
                <div className="min-w-0 flex-1">
                  <p className="truncate text-[0.85rem] font-medium text-ink">{f.name}</p>
                  <p className="truncate text-[0.75rem] text-ink-faint">PDF · {OWNER}</p>
                </div>
                <CardMenu label={`${f.name} options`} />
              </div>
            ))}
          </div>
        )}
      </div>
      </div>
    </div>
  );
}

More patterns

App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.
App screens
Settings panelA real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.
App screens
Data table viewA full documents list screen — status Tabs, a filter toolbar, and a sortable, selectable Table with real recipient avatars.