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.
Installation
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: 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=newestUpload 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.
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.
API
MediaLibrary
| Prop | Type | Default |
|---|---|---|
adapterStorage adapter. Default: the one from MediaProvider / VamosProvider media. | MediaAdapter | — |
view / defaultView / onViewChangeGrid of tiles or a table. | "grid" | "list" | "grid" |
query / defaultQuery / onQueryChangesearch, kind, folderId, month (YYYY-MM), uploadedBy, unattached, sort. URL sync is the app's job. | MediaQuery | — |
selectionnone = browse (click opens details). | "none" | "single" | "multiple" | "multiple" |
selected / defaultSelected / onSelectedChangeSelected 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 | — |
maxSelection limit; other tiles are disabled once reached. | number | — |
detailsPanelWhere file details open (side panel falls back to a dialog under 768px). | "side" | "dialog" | false | "side" |
foldersFolder control in the toolbar or as a sidebar. | "select" | "sidebar" | false | "select" with an adapter that has folders |
pageSizeItems per request; Load more fetches the next cursor. | number | 60 |
tileSizeMinimum tile width (104 / 148 / 212 px); columns follow the container width. | "sm" | "md" | "lg" | "md" |
heightHeight of the scrolling file area. | number | string | "34rem" |
virtualizeAfterVirtualise rows above this many items. | number | 120 |
readOnlyHide upload, delete and editing. | boolean | — |
selectUploadedSelect files as they finish uploading (Media picker sets it). | boolean | false |
maxDimensionDownscale images on the client so the longer side is at most this many pixels. | number | — |
emptyStateReplaces the default “No files yet”. | ReactNode | — |
toolbarShow the toolbar. | boolean | true |
handleRefaddFiles(files), addAssets(assets, { select }), openFileDialog(), reload(). | Ref<MediaLibraryHandle> | — |
searchRefThe search input (Media picker focuses it). | Ref<HTMLInputElement> | — |
onDeletedAfter a confirmed delete. | (ids: string[]) => void | — |
labels / queueLabels / detailsLabelsBuilt-in strings (dictionary namespaces mediaLibrary, mediaUploadQueue, mediaDetails). | Partial<…Labels> | — |
MediaDetails
| Prop | Type | Default |
|---|---|---|
assetThe file to show. | MediaAsset | — |
adapterSaves edits (update, replace, suggestAlt). | MediaAdapter | — |
onChangeAfter each autosave. | (asset) => void | — |
onDelete / onCloseShow Delete / close buttons. | (asset) => void · () => void | — |
autosaveDelayms after typing stops; blur saves at once. | number | 700 |
readOnlyView only. | boolean | — |
MediaUploadQueue
| Prop | Type | Default |
|---|---|---|
queueFrom useUploadQueue(). | UploadQueue | — |
variantCompact strip or a full upload page. | "inline" | "panel" | "inline" |
useUploadQueue(adapter, options?)
| Prop | Type | Default |
|---|---|---|
concurrencyParallel uploads. | number | 3 |
accept / maxFileSize / validateClient-side checks; failures show a reason and can't be retried. | string · number · (file) => string | null | — |
maxDimensionCanvas downscale before upload (JPEG/PNG/WebP). | number | — |
onUploadedPer finished upload. | (asset, item) => void | — |
guardUnloadWarn before leaving while uploads run. | boolean | true |
→ returnsitems[].status: queued | uploading | processing | done | error | canceled. | { add, items, cancel, retry, clearDone, active, done, total } | — |
createRestAdapter(options)
| Prop | Type | Default |
|---|---|---|
baseUrlAPI root; endpoints live under {baseUrl}/media (GET, PATCH /:id, DELETE ?ids=, /uploads, /sideload, /folders, /events). | string | — |
headersStatic or per-request (fresh tokens). | HeadersInit | () => HeadersInit | Promise<HeadersInit> | — |
uploadPresign → XHR PUT with progress → complete; or one multipart POST; or createPresignedUpload({ sign, complete }) for S3/R2. | "presigned" | "multipart" | (file, opts) => Promise<MediaAsset> | "presigned" |
capabilities / loaderWhat the server allows; CDN URL builder. | Partial<MediaCapabilities> · ImageLoader | — |
suggestAlt / eventsPOST /media/:id/suggest-alt · GET /media/events (EventSource). | boolean · "sse" | — |
credentials / fetchCookies; custom fetch for tests and SSR. | RequestCredentials · typeof fetch | — |
createMemoryAdapter(options)
| Prop | Type | Default |
|---|---|---|
seed / foldersInitial data; nothing persists. | MediaAsset[] · MediaFolder[] | — |
latency / uploadDurationSimulated delays (ms). | number | 120 / 900 |
localeSearch folding (tr: İstanbul matches istanbul). | string | "en-US" |