---
title: Files
description: A file manager with a folder tree, multi-select, a context menu, and drag-in upload.
category: TemplateContent
status: beta
since: 0.6.0
tags: [template, files, tree, upload, context-menu]
syncStatus: reviewed
verified: false
---

## Usage

### Workspace files
:::TuffDemoWrapper{demo="TemplateFilesDemo" code-lang="vue" description="Click to select, double-click to open, Space to preview, right-click for the menu; drag files in from the desktop. Sample data; files never leave the browser."}
---
code: |
  <script setup lang="ts">
  import { reactive, ref } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  // Plugin storage follows the host's real rules: 10 MB per file, 100 MB in total, 1,000 files, flat
  const PLUGIN_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/

  // One manual menu for the whole template: tiles and the empty area open it alike
  const menuRef = ref()
  const menu = reactive({ ids: [] as string[], anchorId: null as string | null })
  function onContextMenu(event: MouseEvent) {
    event.preventDefault()
    prepareMenu(event.target) // fills in menu.ids and menu.anchorId
    menuRef.value.openAt(event)
  }

  // TxContextMenu does not hand focus back when it closes; the template returns it to the tile
  function onMenuClose() {
    tileOf(menu.anchorId)?.focus({ preventScroll: true })
  }
  </script>

  <template>
    <TemplateFrame title="Workspace files" :height="580" @enter="syncFromMacBook">
      <TxBreadcrumb :items="crumbs" separator-icon="i-carbon-chevron-right" @click="onCrumb" />

      <TxTree :nodes="folders" :model-value="folder" :expanded-keys="expanded" @update:model-value="open">
        <template #item="{ node, expanded, hasChildren, toggleExpand }">
          <!-- A row click selects; the caret needs .stop or it selects the row too -->
          <button v-if="hasChildren" type="button" tabindex="-1" @click.stop="toggleExpand()">…</button>
          <span :class="expanded ? 'i-carbon-folder-open' : 'i-carbon-folder'" />
          {{ node.label }}
        </template>
      </TxTree>

      <div role="listbox" aria-multiselectable="true" @keydown="onGridKeydown" @contextmenu="onContextMenu">
        <div v-for="file in files" :key="file.id" role="option" :aria-selected="selected.has(file.id)">…</div>
      </div>

      <!-- Laid over the file area while desktop files are dragged in. It only hands over File
           objects: progress, thumbnails and revoking their URLs belong to the host -->
      <div v-show="draggingFiles" class="drop-layer">
        <TxFileUploader ref="uploader" :model-value="[]" multiple @add="queueUploads" />
      </div>

      <TxContextMenu ref="menuRef" trigger="manual" @close="onMenuClose">
        <template #menu>
          <TxContextMenuItem shortcut="F2" @select="rename">Rename</TxContextMenuItem>
          <TxContextMenuDivider />
          <TxContextMenuItem danger shortcut="⌫" @select="remove">Delete</TxContextMenuItem>
        </template>
      </TxContextMenu>

      <!-- Progress changes several times a second: the tray stays silent and one
           sr-only status announces the start and the end instead -->
      <TxToastPanel :open="trayOpen" live="off">…</TxToastPanel>
    </TemplateFrame>
  </template>
---
:::

## Overview

The files of a Tuff workspace, synced across devices, plus a Plugin storage partition held to the host's real limits.

- Selecting: click selects, ⌘ / Ctrl adds, Shift extends, the arrow keys move by row and column, and Esc clears; the keys live on the file area.
- Opening: double-click or Enter opens; Space opens quick look, where ← / → step through; F2 renames; ⌫ / Delete deletes, with Undo.
- Right-click, Shift + F10, or the Menu key opens one menu, anchored to the focused tile from the keyboard; drag tiles onto a folder in the tree to move them.
- Files dragged in from the desktop upload with per-file progress in a tray; a plugin folder refuses files past its limits (10 MB each, 100 MB in total, 1,000 files) or with invalid names.
- The first time it scrolls into view, two screenshots sync in from a MacBook; with reduced motion they are there from the start.
- Expanded (≥ 960px) enlarges the tree and tiles, and from 1200px a preview pane appears; below 640px the tree hides and sub-folders show as tiles.

## Customization

- For real storage, replace the simulated progress with the upload request's progress events; TxFileUploader only receives `File` objects, so validate in the host, sharing one set of constants with the real limits.
- For very large folders, turn the grid into a row-grouped virtual list, and keep the list view on TxDataTable's controlled sorting with server-side paging.
- Show cross-folder search results as a temporary virtual folder, with each item's path.
- When thumbnails come from a server, fix the tile height and set images to `draggable="false"`; revoke object URLs on delete, reset, and unmount.

## Related components

| Region | Components | Role |
|---|---|---|
| Path and toolbar | [TxBreadcrumb](/docs/dev/components/breadcrumb), [TxDropdownMenu](/docs/dev/components/dropdown-menu), [TxIconButton](/docs/dev/components/button), [TxSearchInput](/docs/dev/components/search-input), [TxFlatRadio](/docs/dev/components/flat-radio), [TxButton](/docs/dev/components/button) | Path, search, view, and upload |
| Folder tree | [TxTree](/docs/dev/components/tree) | Controlled tree; drop tiles to move |
| Quota | [TxProgressBar](/docs/dev/components/progress-bar), [TxStatusBadge](/docs/dev/components/status-badge) | Plugin usage and quota bars |
| Grid | [TxIconChip](/docs/dev/components/icon-chip), [TxCornerOverlay](/docs/dev/components/corner-overlay), [TxCheckbox](/docs/dev/components/checkbox), [TxTag](/docs/dev/components/tag) | Thumbnails, badges, and checkboxes |
| List | [TxDataTable](/docs/dev/components/data-table) | Sortable list sharing the selection |
| Context menu | [TxContextMenu / TxContextMenuItem / TxContextMenuDivider / TxContextMenuSubmenu](/docs/dev/components/context-menu) | One menu for tiles and blanks |
| Drag-in upload | [TxFileUploader](/docs/dev/components/file-uploader), [TxToastPanel](/docs/dev/components/toast-panel), [TxCardItem](/docs/dev/components/card-item), [TxProgressBar](/docs/dev/components/progress-bar) | Drop layer and upload tray |
| Preview | [TxModal](/docs/dev/components/modal), [TxMarkdownView](/docs/dev/components/markdown-view), [TxImageGallery](/docs/dev/components/image-gallery), [TxEmptyState](/docs/dev/components/empty-state), [TxSplitter](/docs/dev/components/splitter), [TxNoSelection](/docs/dev/components/no-selection) | Quick look and preview pane |
| Bulk and feedback | [TxButton](/docs/dev/components/button), [TxToastPanel](/docs/dev/components/toast-panel), [TxSearchEmpty](/docs/dev/components/search-empty) | Bulk bar, undo, Clear search |
| Move to… | [TxModal](/docs/dev/components/modal), [TxTree](/docs/dev/components/tree) | Valid target folders only |
