System.Routing

File Manager

The UI layer of a file manager — for cloud drives, media libraries, document managers and IDE sidebars. It owns navigation, selection, keyboard, menus, dialogs, drag and drop and the state of running operations. Your application owns everything else: it handles the events, talks to its storage, and updates items.

Installation

npx raya-ui@latest add file-manager

How it works

The file manager emits intent, your application performs it, and the file manager renders the new items. It never calls an API, reads a disk, stores data or mutates what you pass in. Listening to an event is what enables its action; handlers may return a promise, which the file manager tracks as an operation with progress, errors, retry, cancellation and undo — all of which you control.

user action ─▶ FileManager ─▶ @paste(event, context) ─▶ your API

◀─ progress · error · { undo, select } ─┘

your API ─▶ items ─▶ FileManager renders the result

File Structure

components/ui/file-manager
FileManager.vue— state, navigation, operations, uploads, menus, dialogs
FileManagerToolbar.vue · FileManagerBreadcrumbs.vue— navigation, search, sort, view, actions
FileManagerSidebar.vue— locations + directory tree
FileManagerContent.vue · Card.vue · Row.vue— grid and details views
FileManagerOperations.vue— progress, errors, retry, undo
FileManagerConflictDialog.vue · DeleteDialog.vue— replace / keep both / skip, confirmations
FileManagerMenuItems.vue · FileIcon.vue · StatusBar.vue · RenameInput.vue
FileTree.vue · FileTreeRoot.vue · FileTreeNode.vue— recursive tree (also standalone)
useFileManagerCommands.ts— the action registry
useFileManagerOperations.ts · Conflicts · Loader · Actions— async, conflicts, lazy loading, rename & delete
useFileManagerNavigation · Selection · Keyboard · DragDrop · Search · Item
messages.ts · columns.ts · context.ts · types.ts · utils.ts · variants.ts · index.ts

Usage

Basic usage

Pass a nested tree to items. Every item needs a stable, unique id, a name and a type; folders hold children. Browsing, selection and keyboard navigation work on their own. Give the file manager a height and it fills it.

<script setup lang="ts">
import { FileManager, type FileManagerItem } from '@/components/ui/file-manager'

const files: FileManagerItem[] = [
  {
    id: 'src',
    name: 'src',
    type: 'folder',
    children: [
      { id: 'src/App.vue', name: 'App.vue', type: 'file', size: 734 },
      { id: 'src/main.ts', name: 'main.ts', type: 'file', size: 298 },
    ],
  },
  { id: 'package.json', name: 'package.json', type: 'file', size: 1087 },
]
</script>

<template>
  <FileManager :items="files" class="h-[480px]" />
</template>

Events decide what exists

The file manager emits intent; your application performs it and updates items. Listening to an event is what enables its action — the toolbar, the context menu, the shortcuts and the status bar all come from one action registry, so an action you do not handle never appears, and one the selection, permissions or read-only state forbid is disabled.

<template>
  <FileManager
    :items="files"
    @upload="upload"
    @create-folder="createFolder"
    @create-file="createFile"
    @rename="rename"
    @move="move"
    @paste="paste"
    @duplicate="duplicate"
    @download="download"
    @preview="preview"
    @open="open"
    @trash="trash"
    @restore="restore"
    @delete-permanently="purge"
    @empty-trash="emptyTrash"
    @share="share"
    @copy-link="copyLink"
    @favorite="star"
    @unfavorite="unstar"
    @properties="showProperties"
    @refresh="refresh"
  />
</template>

Async handlers and operation states

Return a promise from any handler and the file manager tracks it: a progress bar in the operations panel and on the affected items, aria-busy, and a spinner on the refresh button. The last argument is a context with progress(percent) and an AbortSignal. Instant actions (rename, star, preview) stay silent unless they fail.

<script setup lang="ts">
import type { FileManagerItem, FileManagerOperationContext } from '@/components/ui/file-manager'

async function download({ items }: { items: FileManagerItem[] }, context: FileManagerOperationContext) {
  const response = await fetch('/api/archive', {
    method: 'POST',
    body: JSON.stringify(items.map(item => item.id)),
    signal: context.signal, // Cancel aborts the request
  })
  const reader = response.body!.getReader()
  const total = Number(response.headers.get('content-length'))
  let received = 0
  for (;;) {
    const { done, value } = await reader.read()
    if (done) break
    received += value.length
    context.progress((received / total) * 100)
  }
  // Saving the file is your call — the file manager never downloads anything.
}
</script>

<template>
  <FileManager :items="files" @download="download" />
</template>

Errors, retry and cancellation

A rejected promise is never swallowed: the operation turns into an error with the message you threw, Retry (which calls your handler again with the same arguments) and Dismiss, and operation-error fires for logging. For partial failures, resolve with failed — Retry then sends only what failed. Cancel aborts context.signal.

<script setup lang="ts">
async function upload(files: File[], folder: FileManagerItem | null, context: FileManagerUploadContext) {
  const results = await Promise.allSettled(files.map((file, i) =>
    storage.put(folder?.id ?? '', context.relativePaths[i] || file.name, file, { signal: context.signal })))

  files.value = await storage.list()
  return {
    failed: results.flatMap((result, i) =>
      result.status === 'rejected' ? [{ source: files[i], error: String(result.reason) }] : []),
  }
}
</script>

<template>
  <FileManager :items="files" @upload="upload" @operation-error="op => logger.warn(op)" />
</template>

Your own operations

When progress lives elsewhere (a resumable upload manager, a job queue), pass operations and listen to cancel-operation, retry-operation and dismiss-operation. Items listed in itemIds show the progress inline.

<script setup lang="ts">
import type { FileManagerOperationState } from '@/components/ui/file-manager'

const operations = computed<FileManagerOperationState[]>(() =>
  uploads.value.map(upload => ({
    id: upload.id,
    type: 'upload',
    status: upload.error ? 'error' : upload.done ? 'success' : 'running',
    label: `Uploading ${upload.name}`,
    progress: upload.percent,
    error: upload.error,
    itemIds: [upload.placeholderId],
    cancelable: true,
    retryable: true,
  })))
</script>

<template>
  <FileManager
    :items="files"
    :operations="operations"
    @cancel-operation="op => uploader.cancel(op.id)"
    @retry-operation="op => uploader.retry(op.id)"
    @dismiss-operation="op => uploader.forget(op.id)"
  />
</template>

Undo and redo

The file manager cannot reverse anything on your server, so it asks you how: return { undo } from a handler and it offers Undo in the panel and on Ctrl/Cmd+Z. Whatever undo returns can carry its own undo, which becomes Redo (Ctrl/Cmd+Shift+Z or Ctrl+Y). message replaces the default success text.

<script setup lang="ts">
async function trash({ items }: FileManagerItemsEvent) {
  const ids = items.map(item => item.id)
  await api.trash(ids)
  files.value = await api.tree()
  return {
    message: `Moved ${ids.length} items to Trash`,
    undo: async () => {
      await api.restore(ids)
      files.value = await api.tree()
      return { undo: () => trash({ items }) } // redo
    },
  }
}
</script>

Copy, cut and paste

Handling @paste enables Copy (Ctrl/Cmd+C), Cut (Ctrl/Cmd+X) and Paste (Ctrl/Cmd+V), from the keyboard, the context menu and on a folder ("paste into"). The clipboard is UI state — bind v-model:clipboard to share it between file managers. Cut items are dimmed until pasted. Paste goes into the open folder; pasting a folder into itself or a read-only folder is refused. copy and cut events tell you what was put on the clipboard.

<script setup lang="ts">
import type { FileManagerClipboard, FileManagerPasteEvent } from '@/components/ui/file-manager'

const clipboard = ref<FileManagerClipboard | null>(null)

async function paste({ items, target, operation, conflicts }: FileManagerPasteEvent, context: FileManagerOperationContext) {
  await (operation === 'cut' ? api.move : api.copy)({
    ids: items.map(item => item.id),
    to: target?.id ?? null,
    // e.g. [{ conflict, action: 'keep-both', name: 'report (1).pdf' }]
    conflicts: conflicts.map(({ conflict, action, name }) => ({ id: (conflict.source as FileManagerItem).id, action, name })),
  }, { signal: context.signal })
  files.value = await api.tree()
}
</script>

<template>
  <!-- Two panes sharing one clipboard -->
  <FileManager v-model:clipboard="clipboard" :items="files" @paste="paste" />
  <FileManager v-model:clipboard="clipboard" :items="files" @paste="paste" />
</template>

Conflict resolution

Before paste, move and upload, the file manager compares names with the destination and asks: Replace, Keep both (with a free name such as "report (1).pdf") or Skip, with "apply to all" for the rest; Cancel stops everything. Copying next to the original keeps both without asking. Your handler receives the choices in conflicts, and skipped items are left out. When your server finds conflicts the file manager cannot see, call context.resolveConflicts() from the handler — the same dialog answers.

<script setup lang="ts">
async function move(event: FileManagerMoveEvent, context: FileManagerOperationContext) {
  const { clashes } = await api.checkMove(event)
  if (clashes.length) {
    const choices = await context.resolveConflicts(clashes.map(clash => ({
      name: clash.name,
      source: event.items.find(item => item.id === clash.id)!,
      destination: null,
      target: event.target,
      reason: clash.locked ? 'permission' : 'exists',
    })))
    if (!choices) return // the user canceled
  }
  await api.move(event)
}
</script>

Download, duplicate, preview and open

Download is a first-class action (toolbar, menu, status bar) for any selection, folders included when your server can archive them. Duplicate (Ctrl/Cmd+D) copies next to the originals. Open and preview are different things: open fires on double-click and Enter; preview is a quick look on Space. The file manager never renders files, runs them or downloads them itself — isPotentiallyUnsafe(item) flags executables so you can warn, and they get no Preview.

<script setup lang="ts">
import { isPotentiallyUnsafe } from '@/components/ui/file-manager'

function open(item: FileManagerItem) {
  if (isPotentiallyUnsafe(item) && !confirm(`${item.name} is an application. Open it anyway?`)) return
  router.push(`/files/${item.id}`)
}
</script>

<template>
  <FileManager
    :items="files"
    @open="open"
    @preview="({ item }) => (quickLook = item)"
    @download="({ items }) => api.download(items)"
    @duplicate="({ items }) => api.duplicate(items)"
  />
  <MyQuickLook v-model:item="quickLook" />
</template>

Permissions and read-only

Give items permissions — read, write, delete, rename, move, copy, download, share, all allowed unless set to false. Actions follow: a folder without write refuses uploads, new items, pastes and drops (and is marked while dragged over); a file without rename has no Rename. readonly hides every change at once. This shapes the UI only — enforce permissions on your server too.

const files: FileManagerItem[] = [
  {
    id: 'shared',
    name: 'Shared with me',
    type: 'folder',
    owner: 'Ada',
    permissions: { write: false, delete: false, rename: false, move: false },
    children: [
      { id: 'shared/roadmap.xlsx', name: 'roadmap.xlsx', type: 'file', permissions: { download: false } },
    ],
  },
]

Trash, restore and permanent delete

With @trash, Delete moves items to the trash without asking (return undo to offer Undo). Items with trashed: true offer Restore and Delete permanently instead, and a listing with trash: true adds Empty Trash; both confirm first. Keep @delete too and Shift+Delete deletes permanently. Without @trash, Delete asks for confirmation and calls @delete.

<template>
  <FileManager
    v-model:location="location"
    :items="files"
    :listing="location === 'trash' ? { items: trashed, trash: true } : null"
    :locations="[{ locations: [{ id: 'trash', label: 'Trash', icon: Trash2, trash: true }] }]"
    @trash="({ items }) => api.trash(items)"
    @restore="({ items }) => api.restore(items)"
    @delete-permanently="({ items }) => api.purge(items)"
    @empty-trash="() => api.emptyTrash()"
  />
</template>

Favorites

Pass the starred ids as favorites and listen to favorite / unfavorite. Starred items show a star, and the menu offers Add to or Remove from Starred. Nothing on the item is mutated.

<template>
  <FileManager
    :items="files"
    :favorites="starred"
    @favorite="({ items }) => starred.push(...items.map(item => item.id))"
    @unfavorite="({ items }) => (starred = starred.filter(id => !items.some(item => item.id === id)))"
  />
</template>

Sidebar locations and listings

Sections above the directory tree come from locations. An entry with folder is a shortcut to a folder (null is the root). Any other entry sets v-model:location, and you show its items through listing — Recent, Starred, Shared with me, a search, a storage provider. Listings are flat and may contain items from anywhere; entries with trash accept drops that move items to the trash.

<script setup lang="ts">
import { Clock, House, Star, Trash2 } from 'lucide-vue-next'

const location = ref<string | null>(null)
const locations = [
  { label: 'Quick access', locations: [
    { id: 'home', label: 'Home', icon: House, folder: null },
    { id: 'recent', label: 'Recent', icon: Clock },
    { id: 'starred', label: 'Starred', icon: Star },
    { id: 'trash', label: 'Trash', icon: Trash2, trash: true },
  ] },
  { label: 'Locations', locations: [
    { id: 'drive', label: 'My Drive', folder: 'drive-root' },
    { id: 'team', label: 'Team Drive', folder: 'team-root' },
  ] },
]

const { data: listing, pending } = useAsyncData(
  () => (location.value ? api.listing(location.value) : Promise.resolve(null)),
  { watch: [location] },
)
</script>

<template>
  <FileManager v-model:location="location" :items="files" :locations="locations" :listing="listing" :loading="pending" />
</template>

Lazy loading large trees

You never need the whole filesystem in memory. With @load-children, a folder whose children is undefined is loaded when it is opened or expanded in the tree (one call, shared by both). A spinner shows while it loads; a rejected promise shows the error with Retry. Set hasChildren: false on folders known to be empty so they show no chevron.

<script setup lang="ts">
const files = ref<FileManagerItem[]>(await api.list(null)) // top level only

async function loadChildren(folder: FileManagerItem, context: FileManagerOperationContext) {
  const children = await api.list(folder.id, { signal: context.signal })
  files.value = setChildren(files.value, folder.id, children) // your immutable update
}
</script>

<template>
  <FileManager :items="files" @load-children="loadChildren" />
</template>

Details view columns

Choose built-in columns — name, modified, created, accessed, type, size, owner, permissions — or add your own with a value (to sort by) and format (to display); the #cell slot renders custom columns any way you like (its fallback is the formatted value). Columns sort from their header or the toolbar Sort menu. When the file manager is narrow, unpinned columns give way from the end so the name keeps at least 12rem; name, size and pinned columns always stay. Drag a column's edge to resize it and a header to move it — or, from the keyboard, arrows on the edge and Alt+Shift+Arrow on a header. Bind v-model:columns to keep the layout; resizable-columns and reorderable-columns turn this off.

<script setup lang="ts">
// Resizing and reordering update this list. To keep the layout across visits,
// store each column's key and width (functions such as `value` cannot be stored).
const columns = ref<(FileManagerColumnKey | FileManagerColumn<Meta>)[]>([
  'name',
  'owner',
  'modified',
  { key: 'version', label: 'Version', width: '5rem', value: (item: FileManagerItem<Meta>) => item.data?.version },
  { key: 'status', label: 'Sync', width: '6rem', pinned: true },
  'size',
])
</script>

<template>
  <!-- v-model: resized and reordered columns come back as the new list. -->
  <FileManager v-model:columns="columns" :items="files" default-view="list">
    <template #cell="{ item, column }">
      <SyncBadge v-if="column.key === 'status'" :state="item.data?.sync" />
    </template>
  </FileManager>
</template>

Command palette

With command-palette, Ctrl/Cmd+K (while focus is in the file manager) opens a searchable list of what can be done to the selection — the same actions as the context menu, with their shortcuts — and every loaded folder and sidebar location to go to. Enter runs the highlighted line and focus returns to the list. It is off by default because many apps own Ctrl/Cmd+K; the exposed openCommandPalette() opens it from your own button, and getActions() feeds a palette of your own instead.

<template>
  <FileManager ref="file manager" :items="files" command-palette @paste="paste" @rename="rename" />
  <Button @click="file manager?.openCommandPalette()">Commands</Button>
</template>

Touch

On touch screens a long-press opens the context menu, and holding an item until it lifts and then moving it drags it — onto a folder, the tree, a breadcrumb or the Trash, with the same checks and highlights as a mouse (browsers have no drag and drop for touch; the file manager replays the drag itself). Moving right away still scrolls. Tap opens folders, check circles stay visible for multi-select, and on phone widths the sidebar becomes an overlay and Download moves to the status bar and menus.

<template>
  <!-- Nothing to configure: touch works wherever `draggable` and @move do. -->
  <FileManager :items="files" draggable @move="move" />
</template>

Large folders

Measured on a production build in Chrome with 5,000 files in one folder: opening it takes under a second, an arrow key about 60 ms, Select all about 160 ms, sorting about 160 ms. Items render in two parts so that focus and selection only touch the items that change, cards and rows use content-visibility, and the clock that ages “5m ago” labels re-renders only labels that change. Beyond a few thousand items per folder, load them in pages with hasMore and @load-more, and load subfolders on demand with @load-children, rather than passing everything at once.

// A page at a time: the file manager asks for the next one as the end scrolls into view.
async function loadMore({ folder, cursor }: FileManagerLoadMoreEvent) {
  const page = await api.list(folder?.id ?? null, { cursor, limit: 500 })
  files.value = appendChildren(files.value, folder?.id ?? null, page.items, { hasMore: page.next !== null, cursor: page.next })
}

Uploads

@upload enables the Upload button, the drop tile and dropping files from the desktop; directory-upload adds Upload folder and walks dropped folders, handing you context.relativePaths. accept and max-file-size are enforced for picked and dropped files alike, and rejected files are reported. Name clashes go through the conflict dialog first.

<template>
  <FileManager
    :items="files"
    accept="image/*,.pdf"
    :max-file-size="50 * 1024 * 1024"
    directory-upload
    @upload="(files, folder, context) => storage.upload(files, folder, context)"
  />
</template>

Translations and RTL

Every visible string — buttons, menus, dialogs, empty states, errors, operation labels, relative dates (timeAgo), file sizes (fileSize) and list separators — comes from messages; pass the ones you want to change. Strings that depend on counts or names are functions, so plurals stay correct. Set dir="rtl" (or use Reka's ConfigProvider) and arrows, indentation, breadcrumbs, menus and column resizing mirror, while file names, sizes and code previews keep their own direction. Turn on “Arabic (RTL)” in the demo settings for a complete translation.

<script setup lang="ts">
import type { FileManagerMessages } from '@/components/ui/file-manager'

const messages: Partial<FileManagerMessages> = {
  newFolder: 'Nouveau dossier',
  upload: 'Téléverser',
  items: count => `${count} élément${count > 1 ? 's' : ''}`,
  deleteTitle: items => `Supprimer ${items.length} élément(s) ?`,
  justNow: 'à l’instant',
  timeAgo: (value, unit) => `il y a ${value} ${{ minute: 'min', hour: 'h', day: 'j', week: 'sem.', month: 'mois', year: 'an' }[unit]}`,
  fileSize: (value, unit) => `${value.replace('.', ',')} ${{ B: 'o', KB: 'Ko', MB: 'Mo', GB: 'Go', TB: 'To' }[unit]}`,
}
</script>

<template>
  <FileManager :items="files" :messages="messages" dir="rtl" />
</template>

FileTree on its own

The directory tree is exported as FileTree: recursive, WAI-ARIA tree semantics through Reka UI, id-based v-model:selected and v-model:expanded, search that reveals matches, inline rename, confirmed delete, lazy @load-children, a context menu and drag and drop.

<script setup lang="ts">
import { FileTree } from '@/components/ui/file-manager'
</script>

<template>
  <FileTree
    v-model:selected="selected"
    v-model:expanded="expanded"
    :items="files"
    searchable
    @rename="(item, name) => api.rename(item.id, name)"
    @load-children="folder => api.loadChildren(folder)"
    class="h-80"
  />
</template>

Keyboard & accessibility

Items form a single-tab-stop listbox (aria-selected, aria-multiselectable, aria-busy while an operation runs); the directory tree is a WAI-ARIA tree. Operations are announced through a polite live region, errors as alerts, progress as progressbar. Focus lands on the new item after create, on the renamed item after rename, on the neighbour after delete, on the first item after opening a folder, and on the folder you came from after Up or Back. Everything drag and drop does is also available as Cut and Paste. Every state (views, menus, dialogs, rename, errors, trash, right to left, light and dark, phone) is checked automatically against WCAG 2.2 AA with axe; automated checks catch a share of issues, so test with a screen reader in your own product too.

← → ↑ ↓

Move (two-dimensional in the grid, mirrored in RTL) and select.

ShiftArrows

Extend the selection.

Ctrl / ⌘Arrows

Move focus only; Ctrl/⌘+Space toggles.

HomeEnd

First / last item.

Enter

Open.

Space

Preview (with @preview), else toggle selection.

AltEnter

Properties.

F2

Rename.

Ctrl / ⌘C · X · V

Copy, cut, paste.

Ctrl / ⌘D

Duplicate.

Delete⌘⌫

Move to trash, or delete after confirmation.

ShiftDelete

Delete permanently.

Ctrl / ⌘Z

Undo. Ctrl+Y or Ctrl/⌘+Shift+Z redo.

Ctrl / ⌘A

Select all.

BackspaceAlt ↑

Up, selecting the folder you left.

Alt← / →

Back, forward.

Esc

Clear the selection.

a–z

Type-ahead.

Ctrl / ⌘K

Command palette (with command-palette).

AltShift← / →

On a column header: move the column.

← / →HomeEnd

On a column edge: resize (Shift for bigger steps); Enter resets.

Ctrl / ⌘C · X · V

In the directory tree: copy, cut, paste into the focused folder.

API Reference

Props

items
FileManagerItem<TData>[]
[]

The tree. Never mutated.

folder
string | null
null

Open folder id (null is the root). v-model:folder.

selected
string[]
[]

Selected ids. v-model:selected.

view
"grid" | "list"
"grid"

v-model:view.

sort
FileManagerSort
{ key: "name", direction: "asc" }

v-model:sort. Folders always first.

search
string
""

v-model:search. Filters the open folder, or runs @search with remote-search.

clipboard
FileManagerClipboard | null
null

v-model:clipboard. Share it between file managers.

location
string | null
null

Active sidebar location without a folder. v-model:location.

listing
FileManagerListing | null
null

Flat items shown instead of the open folder: Recent, Starred, Trash, search results.

locations
FileManagerLocationSection[]
[]

Sidebar sections above the directory tree.

operations
FileManagerOperationState[]
—

Your own operations, shown with the file manager's.

columns
(FileManagerColumnKey | FileManagerColumn)[]
name, modified, type, size

Details view columns. v-model:columns receives resized and moved columns.

resizableColumns · reorderableColumns
boolean
true

Resize columns from their edge; move them by their header.

commandPalette
boolean
false

Ctrl/Cmd+K palette of actions and places.

defaultFolder · defaultSelected · defaultView · defaultSort
—
—

Initial state when the matching v-model is not bound.

favorites
string[]
—

Starred ids.

multiple
boolean
true

Multi-selection.

draggable
boolean
false

Drag and drop between folders, the tree, breadcrumbs and Trash.

sidebar
boolean
true

Locations and directory tree (an overlay on narrow file managers).

readonly
boolean
false

Hides every action that changes data.

loading
boolean
false

Skeletons and aria-busy.

disabled
boolean
false

Disables everything.

accept
string
—

Accepted uploads, for picked and dropped files.

maxFileSize
number
—

Largest accepted upload, in bytes.

directoryUpload
boolean
false

Upload folder, and dropped folders.

remoteSearch
boolean
false

Search with @search instead of filtering.

searchDebounce
number
300

Delay before @search, in ms.

confirmDelete
boolean
true

Confirm deletes in a dialog.

validateName
(name, item) => string | undefined
—

Extra rename checks.

getIcon
FileManagerIconResolver
—

Custom icons.

messages
Partial<FileManagerMessages>
—

Every user-facing string.

dir
"ltr" | "rtl"
inherited

Reading direction.

rootLabel · label · class
string
"root" · "Files"

Breadcrumb root, list accessible name, root classes.

Item

id · name · type
string · string · "file" | "folder"

Required. Ids are stable and unique across the tree.

children
FileManagerItem[]

A folder's contents. With @load-children, undefined means "not loaded".

hasChildren · hasMore · cursor
boolean · boolean · unknown

Lazy loading and pagination.

size
number

Bytes.

createdAt · modifiedAt · accessedAt
Date | string

Shown relative; used for sorting.

owner · mimeType · extension
string

Metadata for columns, icons and the status bar.

description · preview · thumbnail
string

Card subtitle, text preview, image URL.

permissions
FileManagerPermissions

read, write, delete, rename, move, copy, download, share.

trashed · disabled
boolean

In the trash; not interactive.

data
TData

Your metadata, typed everywhere.

Events

Action events are handler props: listening to one enables the action. Each receives a FileManagerOperationContext last and may return (or resolve to) a result.

upload
(files, folder, context)

Picked or dropped files. context has relativePaths and conflicts.

create-folder · create-file
(parent, context)

Return { rename: id } to rename the new item.

rename
(item, name, context)

Inline rename (F2).

move
({ items, target, conflicts }, context)

Drag and drop.

paste
({ items, target, operation, conflicts }, context)

Enables Copy, Cut and Paste.

duplicate · download · share · copy-link · properties
({ items }, context)

Actions on the selection.

preview
({ item }, context)

Quick look (Space).

open
item

Double-click, Enter, status bar.

delete
(items, context)

After confirmation. Shift+Delete when @trash is also set.

trash · restore · delete-permanently
({ items }, context)

Trash workflow.

empty-trash
({ folder }, context)

From a trash listing, after confirmation.

favorite · unfavorite
({ items }, context)

Starring.

refresh
({ folder }, context)

Toolbar refresh.

load-children
(folder, context)

Lazy folders.

load-more
({ folder, cursor }, context)

Next page.

search
({ query, folder }, context)

With remote-search.

copy · cut
{ items }

Items put on the clipboard.

cancel-operation · retry-operation · dismiss-operation
FileManagerOperationState

For your own operations.

operation-error
FileManagerOperationState

One of the file manager's operations failed.

update:*
folder, selected, view, sort, search, clipboard, location, columns

v-model updates.

Handler results

select
string[]

Select and focus these ids once they appear in items.

rename
string

Start renaming this id once it appears.

undo
() => FileManagerHandlerResult

Offer Undo; its own result may carry undo (Redo).

message
string

Success text in the operations panel.

failed
{ source, error }[]

Partial failure; Retry sends only these.

Slots

#context-menu
{ item, actions, rename, remove, defer }

Replaces the built-in menu.

#preview
{ item }

A card's preview area.

#cell
{ item, column }

Custom details columns.

#empty
{ query }

Empty folder or no matches.

#delete-description
{ items }

Delete confirmation text.

#toolbar-actions
—

Extra toolbar buttons.

#status-actions
{ items, actions }

Status bar actions.

Exposed

rename(id) · remove(ids)

Inline rename; delete through the dialog.

copy(ids) · cut(ids) · paste(folderId?)

Clipboard.

undo() · redo() · refresh()

History and reload.

resolveConflicts(conflicts)

Open the conflict dialog yourself.

getActions(ids?)

Available actions, e.g. for your own command palette.

openCommandPalette() · focus()

Open the built-in palette; move focus into the file manager.

operations

Every operation currently shown.

TypeScript

Everything is exported from @/components/ui/file-manager: FileManagerItem, FileManagerPermissions, FileManagerOperationState, FileManagerOperationContext, FileManagerOperationResult, FileManagerPasteEvent, FileManagerMoveEvent, FileManagerItemsEvent (download, duplicate, trash…), FileManagerConflict and its resolution, FileManagerClipboard, FileManagerListing, FileManagerLocation, FileManagerColumn, FileManagerAction, FileManagerCommand, FileManagerMessages (with FileManagerTimeUnit and FileManagerSizeUnit), and the helpers getFileKind, isPotentiallyUnsafe, can, formatBytes, formatRelativeTime, uniqueName, matchesAccept and sortFileItems. Both components are generic over TData.

Migrating from the previous version

  • Handlers get a trailing context argument (signal, progress, resolveConflicts). Existing handlers keep working.
  • move is now a handler: @move="fn" is unchanged in templates, and the payload gains conflicts. wrapper.emitted("move") no longer records it in tests.
  • The status bar's default action reads "Open" (it was "Open File"), and the New Folder button's label is "New Folder" — both configurable through messages.
  • FileManagerSort.key also accepts custom column keys.
  • A built-in context menu now appears when you do not provide #context-menu. Its scope gained actions and defer.
  • The Type column shows the kind of item ("TypeScript", "PNG image"), and the status bar shows the kind instead of the MIME type. item.description is free text for the card subtitle; add a column with value: item => item.description to show it in the details view.
  • Menus are no longer modal: like desktop context menus they do not trap focus or hide the page from assistive technology, and clicking elsewhere both closes them and acts.
  • The rename input is drawn over the item (in a layer inside the file manager) instead of inside it, so tests that look for it inside an option or tree item should look in the file manager.
  • FileManagerConflictReason gained "into-itself", used when a folder is pasted into itself; the conflict dialog explains it.
  • Relative dates and file sizes are translatable (justNow, timeAgo, fileSize, listSeparator); formatBytes and formatRelativeTime accept the messages as an optional last argument.

ui-primitives

Folder, 14 items, 1d ago

Directory

Dialog.vue, Button.vue

+12

Folder1d ago

FileManager.vue

Vue component, 4.2 KB, 10m ago, Starred

Vue 3 SFC

<template>
<FileManager v-model="active" />
4.2 KB10m ago

hero-banner.png

PNG image, 1.4 MB, 3d ago

Raster asset

1.4 MB3d ago

nuxt.config.ts

TypeScript, 1.1 KB, 4d ago

Core config

export default defineNuxtConfig({
devtools: { enabled: true }
1.1 KB4d ago

useFileSystem.ts

TypeScript, 2.8 KB, 2h ago

Composable hook

export const useFS = () =>
return { readTree }
2.8 KB2h ago

FileManager.vue

Mock server in memory — try uploading a file named “fail.txt”.

Settings
Directory treeSidebar with locations and folders.
Multiple selectionCtrl/Cmd, Shift and Ctrl+A.
Drag and dropMove items onto folders or Trash.
Read onlyHides every action that changes data.
Slow networkMakes progress easy to watch.
LoadingShow skeleton cards.
Arabic (RTL)Translated messages, right to left.