Raccourci clavier : Ctrl + K
Commencer

Champ de fichiers

La sélection de fichiers en champ de formulaire : un champ de texte en lecture seule par-dessus un champ fichier masqué, qui accepte aussi un dépôt. La valeur est toujours une liste de fichiers, que plusieurs soient permis ou non.

Utilisation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

const files = ref<File[]>([])
</script>

<template>
  <VFileInput v-model="files" label="Attachment" />
</template>

Exemples

Libellé et aide

label, hint et placeholder se comportent comme sur n'importe quel autre champ. iconStart pose une icône au début du champ, rendue avant les puces et non à leur place.

vue

Drop a file on the field, or use the paperclip

contract.pdfannex.pdf

An icon at the start, the paperclip at the end

<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'
import { search } from 'vectis-ui/icons'

const attachment = ref<File[]>([])
const found = ref<File[]>([
  new File(['x'], 'contract.pdf', { type: 'application/pdf' }),
  new File(['x'], 'annex.pdf', { type: 'application/pdf' }),
])
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="attachment"
      label="Attachment"
      hint="Drop a file on the field, or use the paperclip"
      placeholder="No file chosen yet"
    />

    <!-- The start icon is rendered before the chips, so a chip display keeps both. -->
    <VFileInput
      v-model="found"
      multiple
      display="chip"
      :icon-start="search"
      label="Search the attachments"
      hint="An icon at the start, the paperclip at the end"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Tailles

size définit la hauteur du champ à 32, 40 ou 48 pixels, et compact lui retire 4px. Les puces des fichiers choisis se placent un palier en dessous du champ.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

const rows = ref(
  (['sm', 'md', 'lg'] as const).flatMap((size) => [
    { key: size, size, compact: false, label: size, files: [] as File[] },
    { key: `${size}-compact`, size, compact: true, label: `${size}, compact`, files: [] as File[] },
  ]),
)
</script>

<template>
  <div class="column">
    <VFileInput
      v-for="row in rows"
      :key="row.key"
      v-model="row.files"
      :size="row.size"
      :compact="row.compact"
      :label="row.label"
      multiple
      display="chip"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Fichiers multiples

multiple permet au champ de prendre plusieurs fichiers. Le modèle est un tableau de File dans les deux cas.

vue

Drop two and the second is turned away

The model is a File array either way, so nothing downstream has to branch

0 fichier
<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

const one = ref<File[]>([])
const several = ref<File[]>([])
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="one"
      label="One file, the default"
      hint="Drop two and the second is turned away"
    />

    <VFileInput
      v-model="several"
      multiple
      display="chip"
      counter
      label="Several files"
      hint="The model is a File array either way, so nothing downstream has to branch"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Effacement

clearable ajoute une croix qui vide toute la sélection d'un coup.

vue

Add a file: the cross appears to the left of the paperclip

Files then come out one chip at a time

<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

const withCross = ref<File[]>([])
const withoutCross = ref<File[]>([])
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="withCross"
      clearable
      multiple
      display="chip"
      label="Clearable"
      hint="Add a file: the cross appears to the left of the paperclip"
    />

    <VFileInput
      v-model="withoutCross"
      multiple
      display="chip"
      label="Not clearable, the default"
      hint="Files then come out one chip at a time"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Affichage

display liste les fichiers en noms séparés par des virgules, ou en une puce supprimable chacun. Le slot #chip remplace une puce et reçoit son libellé raccourci, remove, ainsi que la taille et la densité calculées par le champ.

vue

The names joined by commas, on one line

quarterly-report.pdfbalance-sh…l-v3.xlsx

One dismissible chip each, cut in the middle so the extension survives

quarterly-report.pdfbalance-sh…l-v3.xlsx
<script setup lang="ts">
import { ref } from 'vue'
import { VChip, VFileInput } from 'vectis-ui'
import { description, picture_as_pdf as pictureAsPdf } from 'vectis-ui/icons'

/* Two files to start with, so the three fields read before anything is picked. A real
   selection comes from the dialog or from a drop. */
const start = () => [
  new File(['x'], 'quarterly-report.pdf', { type: 'application/pdf' }),
  new File(['x'], 'balance-sheet-2026-final-v3.xlsx'),
]

const asText = ref(start())
const asChips = ref(start())
const custom = ref(start())
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="asText"
      multiple
      label="text, the default"
      hint="The names joined by commas, on one line"
    />

    <VFileInput
      v-model="asChips"
      multiple
      display="chip"
      label="chip"
      hint="One dismissible chip each, cut in the middle so the extension survives"
    />

    <VFileInput v-model="custom" multiple display="chip" label="a chip of your own">
      <template #chip="{ file, label, remove, size, compact }">
        <VChip
          :icon-start="file.type === 'application/pdf' ? pictureAsPdf : description"
          :size="size"
          :compact="compact"
          :dismiss-label="`Remove ${file.name}`"
          variant="outline"
          tone="accent"
          dismissible
          @dismiss="remove"
        >
          {{ label }}
        </VChip>
      </template>
    </VFileInput>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Limites par fichier

accept accepte la syntaxe du navigateur et filtre la boîte de dialogue système comme un fichier déposé sur le champ. maxSize borne un fichier. Un fichier refusé n'entre jamais dans le modèle, et reject est émis une fois par fichier.

vue

Images and PDFs, 500 kB each at most

<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput, type FileRejection } from 'vectis-ui'

const files = ref<File[]>([])
const refused = ref<FileRejection[]>([])

/* One event per file, so a batch drop is reported precisely rather than as a single
   "something went wrong". The wording is yours: the component never writes it. */
const reasons: Record<string, string> = {
  type: 'wrong kind of file',
  size: 'too big on its own',
  count: 'too many files',
  'total-size': 'too much altogether',
}

function onReject(rejection: FileRejection) {
  refused.value = [rejection, ...refused.value].slice(0, 4)
}
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="files"
      accept="image/*,.pdf"
      :max-size="500_000"
      multiple
      display="chip"
      label="Receipts"
      hint="Images and PDFs, 500 kB each at most"
      @reject="onReject"
    />

    <output v-if="refused.length" class="refused" aria-label="Files turned away">
      <span v-for="item in refused" :key="item.file.name + item.reason">
        {{ item.file.name }}: {{ reasons[item.reason] }}
      </span>
    </output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.refused {
  display: grid;
  gap: var(--vectis-space-1);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

Limites de sélection

maxFiles et maxTotalSize bornent la sélection dans son ensemble. Le filtrage suit un ordre fixe : type, puis taille, puis nombre, puis taille totale.

vue

Three files at most, 1 MB altogether

0 fichier
<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput, type FileRejection } from 'vectis-ui'

const files = ref<File[]>([])
const refused = ref<FileRejection[]>([])

const reasons: Record<string, string> = {
  type: 'wrong kind of file',
  size: 'too big on its own',
  count: 'that would be a fourth file',
  'total-size': 'that would take the batch over 1 MB',
}

function onReject(rejection: FileRejection) {
  refused.value = [rejection, ...refused.value].slice(0, 4)
}
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="files"
      :max-files="3"
      :max-total-size="1_000_000"
      multiple
      display="chip"
      counter
      clearable
      label="Invoices"
      hint="Three files at most, 1 MB altogether"
      @reject="onReject"
    />

    <output v-if="refused.length" class="refused" aria-label="Files turned away">
      <span v-for="item in refused" :key="item.file.name + item.reason">
        {{ item.file.name }}: {{ reasons[item.reason] }}
      </span>
    </output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.refused {
  display: grid;
  gap: var(--vectis-space-1);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

Compteur

counter ajoute une ligne sous le champ indiquant ce qui a été choisi. Le slot #counter la remplace et reçoit le nombre, le total en octets et la phrase déjà construite. Là où le counter de VInput et VTextarea compte des caractères sans slot, celui-ci compte des fichiers.

vue
quarterly-report.pdfcover-photo.jpg

It sits under the field, to the right of this line

2 fichiers (336 ko)
quarterly-report.pdfcover-photo.jpg
2 of 5 · 336 kB
<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

/* A real byte length, so the figure the counter prints is a real one. */
const start = () => [
  new File([new Uint8Array(240_000)], 'quarterly-report.pdf', { type: 'application/pdf' }),
  new File([new Uint8Array(96_000)], 'cover-photo.jpg', { type: 'image/jpeg' }),
]

const standard = ref(start())
const custom = ref(start())
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="standard"
      counter
      multiple
      display="chip"
      label="The counter as it comes"
      hint="It sits under the field, to the right of this line"
    />

    <VFileInput v-model="custom" counter multiple display="chip" label="A wording of your own">
      <template #counter="{ count, bytes }">
        <span class="own">{{ count }} of 5 · {{ Math.round(bytes / 1000) }} kB</span>
      </template>
    </VFileInput>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
.own {
  font-variant-numeric: tabular-nums;
}
</style>

Icône personnalisée

pickerIcon est le glyphe en fin de champ qui ouvre la boîte de dialogue système, et il accepte toute valeur d'icône.

vue

pickerIcon takes the same values as every icon prop in the library

Naming the icon after what the field accepts says more than a paperclip

<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'
import { cloud_upload as cloudUpload, image } from 'vectis-ui/icons'

const documents = ref<File[]>([])
const photos = ref<File[]>([])
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="documents"
      :picker-icon="cloudUpload"
      multiple
      display="chip"
      label="Documents"
      hint="pickerIcon takes the same values as every icon prop in the library"
    />

    <VFileInput
      v-model="photos"
      :picker-icon="image"
      accept="image/*"
      multiple
      display="chip"
      label="Photos"
      hint="Naming the icon after what the field accepts says more than a paperclip"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

États

invalid marque le champ en erreur. readonly garde la sélection visible et refuse toute façon de la changer. disabled grise le champ et le sort de l'ordre de tabulation. noDrop n'écarte que le dépôt, et loading est purement visuel, un indicateur remplaçant l'icône de pièce jointe. pickerIconLabel, clearLabel et loadingText renomment ce que chacun annonce.

vue

For a rule of your own: nothing here is checked by the browser

What was chosen stays on show, and no dialog, drop or removal will change it

Greyed through the colour tokens, and out of the tab order

Chargement…

A spinner where the paperclip was, and nothing else changes

The paperclip still opens the dialog: only dropping is turned away

<script setup lang="ts">
import { ref } from 'vue'
import { VFileInput } from 'vectis-ui'

const chosen = () => [new File(['x'], 'contract.pdf', { type: 'application/pdf' })]

const invalid = ref(chosen())
const readOnly = ref(chosen())
const disabled = ref(chosen())
const noDrop = ref<File[]>([])
const uploading = ref(chosen())
</script>

<template>
  <div class="column">
    <VFileInput
      v-model="invalid"
      invalid
      display="chip"
      label="Invalid"
      hint="For a rule of your own: nothing here is checked by the browser"
    />

    <VFileInput
      v-model="readOnly"
      readonly
      display="chip"
      label="Read-only"
      hint="What was chosen stays on show, and no dialog, drop or removal will change it"
    />

    <VFileInput
      v-model="disabled"
      disabled
      display="chip"
      label="Disabled"
      hint="Greyed through the colour tokens, and out of the tab order"
    />

    <!-- Purely visual: files can still be dropped and the dialog still opens. -->
    <VFileInput
      v-model="uploading"
      loading
      display="chip"
      label="Uploading"
      hint="A spinner where the paperclip was, and nothing else changes"
    />

    <VFileInput
      v-model="noDrop"
      no-drop
      multiple
      display="chip"
      label="Drop refused"
      hint="The paperclip still opens the dialog: only dropping is turned away"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDéfaut
multiplebooleanfalse
Permet de choisir plusieurs fichiers. Avec un seul, tout fichier supplémentaire est écarté.
acceptstringaucune
Quels genres de fichiers sont acceptés, dans la syntaxe du navigateur. C'est appliqué DEUX FOIS, et il le faut : en attribut, ce qui filtre la boîte de dialogue du système, et de nouveau en code, seule chose capable de filtrer un fichier DÉPOSÉ.
displayFileInputDisplay'chip' | 'text''text'
Comment les fichiers choisis sont montrés : leurs noms joints par des virgules, ou une puce retirable chacun. Cela ne signifie quelque chose que si plusieurs fichiers sont permis ; un nom seul est toujours du texte.
maxSizenumberaucune
La taille maximale d'UN fichier, en octets.
maxTotalSizenumberaucune
La taille maximale de toute la sélection, en octets.
maxFilesnumberaucune
Combien de fichiers peuvent être choisis au plus.
counterbooleanfalse
Affiche ce qui a été choisi sous le champ, « 3 fichiers (1,2 Mo) ».
pickerIconIconSourceattach_file
L'icône en fin de champ, qui ouvre la boîte de dialogue de fichiers.
noDropbooleanfalse
Refuse les fichiers déposés sur le composant : seule la boîte de dialogue en ajoute alors.
sizeFileInputSize'sm' | 'md' | 'lg''md'
La hauteur du champ : 32, 40 ou 48 pixels.
compactbooleanfalse
Retire 4px à la hauteur, en laissant le rembourrage, le texte et les icônes tels quels.
disabledbooleanfalse
Rend le champ inutilisable, grisé par les tokens de couleur.
readonlybooleanfalse
Montre ce qui a été choisi sans permettre de le changer : ni boîte de dialogue, ni dépôt, ni retrait.
invalidbooleanfalse
Marque le champ comme invalide, pour une règle à vous, puisque rien ici n'est vérifié par le navigateur.
labelstringaucune
Le libellé au-dessus du champ, lié à lui pour qu'un clic dessus y place le focus.
hintstringaucune
Une ligne d'aide sous le champ, à gauche du compteur. Elle est liée au champ pour les technologies d'assistance.
placeholderstringaucune
Ce que dit le champ tant que rien n'est choisi. Il retombe sur le dictionnaire du design system.
iconStartIconSourceaucune
Une icône dans le champ, au début. Elle est rendue avant les chips et non à leur place. Décorative jusqu'à ce qu'un écouteur @click:icon-start en fasse un bouton.
iconStartLabelstringaucune
Ce que fait l'icône de début, en mots, une fois cliquable.
pickerIconLabelstringaucune
Ce que fait l'icône de fin, en mots. Elle nomme le bouton qui ouvre la boîte de dialogue de fichiers, et sa valeur par défaut vient du dictionnaire du design system.
loadingbooleanfalse
Affiche une roue à la place de l'icône de trombone, pendant un envoi. Elle ne change rien d'autre : les fichiers se déposent toujours et la boîte de dialogue s'ouvre toujours.
loadingTextstringaucune
Ce que les lecteurs d'écran annoncent pendant que la roue tourne. Sa valeur par défaut vient du dictionnaire du design system.
clearablebooleanfalse
Propose une croix qui vide la sélection. Elle vaut plus la peine ici que sur un champ ordinaire : ce que contient un sélecteur ne s'efface pas en tapant, la croix est donc le seul retour en arrière après un mauvais choix.
clearLabelstringaucune
Ce que fait cette croix, en mots. Sa valeur par défaut vient du dictionnaire du design system.
v-modelFile[][]
Toujours une LISTE de fichiers, que plusieurs soient permis ou non, jamais un fichier seul. La forme ne dépend pas d'une prop : vous n'avez donc jamais à restreindre une union que TypeScript ne sait pas discriminer.

Événements

ÉvénementType
click:icon-start[event: MouseEvent]
L'icône de début a été cliquée. Attacher cet écouteur est ce qui en fait un vrai bouton, qui demande alors iconStartLabel.
clear[]
La croix d'effacement a été pressée. La sélection est déjà vide.
change[files: File[]]
La sélection a changé, avec toute la liste telle qu'elle est désormais.
reject[rejection: FileRejection]
Un fichier a été écarté, avec lequel et pourquoi : son genre, sa taille, ou combien il y en avait déjà.
remove[file: File, index: number]
UN fichier a été retiré par sa puce, avec le fichier et la position qu'il occupait. change suit avec toute la liste.

Slots

SlotType
start{}
Du contenu au début du champ, rendu après iconStart plutôt qu'à sa place.
value-end{}
Des contrôles à vous à l'intérieur du champ, placés avant ceux que le champ possède : la croix d'effacement et l'icône qui ouvre le panneau. Ces deux-là sont l'affordance propre du composant, ce qui explique l'absence de slot end ici.
chipFileInputChipSlotProps
Remplace la puce qui représente un fichier. Elle reçoit le nom déjà raccourci AU MILIEU pour que son extension survive, remove, sans quoi le fichier ne pourrait plus être retiré, et la taille et la densité calculées pour tenir dans le champ.
counterFileInputCounterSlotProps
Remplace le compteur sous le champ. text est la phrase déjà construite et traduite ; le nombre et la taille totale en octets, bytes, sont là pour une formulation à vous.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type ChipSize = 'xs' | 'sm'
export interface FileInputChipSlotProps {
  file: File
  index: number
  label: string
  remove: () => void
  size: ChipSize
  compact: boolean
}
export interface FileInputCounterSlotProps {
  count: number
  bytes: number
  text: string
}
export type FileRejectReason = 'type' | 'size' | 'count' | 'total-size'
export interface FileRejection {
  file: File
  reason: FileRejectReason
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender