Media library

Browse, search, filter, upload, edit and delete files through any storage adapter: grid or list, folders, an upload queue with progress and retry, a details panel with alt text, focal point and usage, bulk actions, and a virtualised grid for libraries with thousands of files. It is also the body of Media picker and works as a document archive in CRM and HR.

MediaAdapter contract · @tanstack/react-virtual · XHR uploads media/media-library.tsx, media/use-upload-queue.ts, media/rest-adapter.ts, media/memory-adapter.ts, media/media-utils.ts, media/types.tstheme · material · accent · density · font
No files selected

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { MediaLibrary, MediaProvider, createRestAdapter } from "@vamos-ui/react/media";

const media = createRestAdapter({ baseUrl: "/api", headers: () => ({ Authorization: `Bearer ${token()}` }) });

<MediaProvider adapter={media}>
  <MediaLibrary selection="multiple" detailsPanel="side" onQueryChange={syncToUrl} />
</MediaProvider>

Keyboard (grid): arrows move, Home/End (row), Ctrl/⌘ + Home/End (all), Page Up/Down, Space selects, Shift extends, Ctrl/⌘ + A selects all, Enter opens details, Delete asks to delete, Esc clears the selection. The grid uses a roving tabindex, also when virtualised.

Drop files anywhere on the library, or paste them; the whole area highlights while dragging. Uploads warn before the page is closed.

A warning dot marks images without alt text. “Decorative image” in details clears the alt text and tags the asset decorative, so it stops counting as missing.

Tiles never use backdrop blur: the library is one surface and tiles are flat surface-2, so hundreds of thumbnails stay smooth in glass mode.

Sizes, dates, months and percentages follow the locale (tr: 2,4 MB, %42, Kasım 2026).

Examples

List view (HR documents)

A real <table> with type, size, date and alt status. accept narrows the library and the uploads to documents.

Folders sidebar (CMS media screen)

folders="sidebar" lists the adapter's folders with counts and a New folder action; selection="none" makes a click open details.

Bulk actions

Shift + click or Shift + arrows for ranges, Ctrl/⌘ + A for all. The bar moves, downloads or deletes (behind a confirm).

3 selected
3 files selected

3 selected: m_lobby, m_bistro, m_meeting

Controlled filters (URL sync)

query + onQueryChange make search, type, date, folder, uploader, Unattached and sort controllable; write them to the URL.

/admin/media?kind=image&sort=newest
No files selected

Upload queue: errors and retry

Three uploads at a time with progress; wrong types and files over capabilities.maxFileSize are rejected with a reason. maxDimension downscales large photos on the client.

No files selected

Read-only

readOnly (or an adapter without edit) hides upload, delete and editing; details stay viewable.

Empty library

First use: drop files anywhere or press Upload.

No files selected

API

MediaLibrary

PropTypeDefault
adapter
Storage adapter. Default: the one from MediaProvider / VamosProvider media.
MediaAdapter—
view / defaultView / onViewChange
Grid of tiles or a table.
"grid" | "list""grid"
query / defaultQuery / onQueryChange
search, kind, folderId, month (YYYY-MM), uploadedBy, unattached, sort. URL sync is the app's job.
MediaQuery—
selection
none = browse (click opens details).
"none" | "single" | "multiple""multiple"
selected / defaultSelected / onSelectedChange
Selected ids in order, plus the assets the library knows for them.
string[] · (ids, assets) => void—
accept
"image/*,.pdf": filters the library and restricts uploads.
string—
max
Selection limit; other tiles are disabled once reached.
number—
detailsPanel
Where file details open (side panel falls back to a dialog under 768px).
"side" | "dialog" | false"side"
folders
Folder control in the toolbar or as a sidebar.
"select" | "sidebar" | false"select" with an adapter that has folders
pageSize
Items per request; Load more fetches the next cursor.
number60
tileSize
Minimum tile width (104 / 148 / 212 px); columns follow the container width.
"sm" | "md" | "lg""md"
height
Height of the scrolling file area.
number | string"34rem"
virtualizeAfter
Virtualise rows above this many items.
number120
readOnly
Hide upload, delete and editing.
boolean—
selectUploaded
Select files as they finish uploading (Media picker sets it).
booleanfalse
maxDimension
Downscale images on the client so the longer side is at most this many pixels.
number—
emptyState
Replaces the default “No files yet”.
ReactNode—
toolbar
Show the toolbar.
booleantrue
handleRef
addFiles(files), addAssets(assets, { select }), openFileDialog(), reload().
Ref<MediaLibraryHandle>—
searchRef
The search input (Media picker focuses it).
Ref<HTMLInputElement>—
onDeleted
After a confirmed delete.
(ids: string[]) => void—
labels / queueLabels / detailsLabels
Built-in strings (dictionary namespaces mediaLibrary, mediaUploadQueue, mediaDetails).
Partial<…Labels>—

MediaDetails

PropTypeDefault
asset
The file to show.
MediaAsset—
adapter
Saves edits (update, replace, suggestAlt).
MediaAdapter—
onChange
After each autosave.
(asset) => void—
onDelete / onClose
Show Delete / close buttons.
(asset) => void · () => void—
autosaveDelay
ms after typing stops; blur saves at once.
number700
readOnly
View only.
boolean—

MediaUploadQueue

PropTypeDefault
queue
From useUploadQueue().
UploadQueue—
variant
Compact strip or a full upload page.
"inline" | "panel""inline"

useUploadQueue(adapter, options?)

PropTypeDefault
concurrency
Parallel uploads.
number3
accept / maxFileSize / validate
Client-side checks; failures show a reason and can't be retried.
string · number · (file) => string | null—
maxDimension
Canvas downscale before upload (JPEG/PNG/WebP).
number—
onUploaded
Per finished upload.
(asset, item) => void—
guardUnload
Warn before leaving while uploads run.
booleantrue
→ returns
items[].status: queued | uploading | processing | done | error | canceled.
{ add, items, cancel, retry, clearDone, active, done, total }—

createRestAdapter(options)

PropTypeDefault
baseUrl
API root; endpoints live under {baseUrl}/media (GET, PATCH /:id, DELETE ?ids=, /uploads, /sideload, /folders, /events).
string—
headers
Static or per-request (fresh tokens).
HeadersInit | () => HeadersInit | Promise<HeadersInit>—
upload
Presign → XHR PUT with progress → complete; or one multipart POST; or createPresignedUpload({ sign, complete }) for S3/R2.
"presigned" | "multipart" | (file, opts) => Promise<MediaAsset>"presigned"
capabilities / loader
What the server allows; CDN URL builder.
Partial<MediaCapabilities> · ImageLoader—
suggestAlt / events
POST /media/:id/suggest-alt · GET /media/events (EventSource).
boolean · "sse"—
credentials / fetch
Cookies; custom fetch for tests and SSR.
RequestCredentials · typeof fetch—

createMemoryAdapter(options)

PropTypeDefault
seed / folders
Initial data; nothing persists.
MediaAsset[] · MediaFolder[]—
latency / uploadDuration
Simulated delays (ms).
number120 / 900
locale
Search folding (tr: İstanbul matches istanbul).
string"en-US"