nb-input-file

File picker component for Vue.js 3+. It uses a native <input type="file"> (visually hidden) plus a themed control. Validation covers accept, extensions, size, duplicates, and optional GIF dimension / video metadata rules. The component emits File[] for previews or uploads in the parent.

Loading component...

Installation

Yarn
yarn add @vlalg-nimbus/nb-inputs
NPM
npm install @vlalg-nimbus/nb-inputs

Usage

Vue 3
import { createApp } from 'vue'
import App from './App.vue'

import NbInputComponents from '@vlalg-nimbus/nb-inputs'
import "@vlalg-nimbus/nb-inputs/dist/style.css";

const app = createApp(App)
app.use(NbInputComponents)
app.mount('#app')
Nuxt 3
import NbInputComponents from '@vlalg-nimbus/nb-inputs'
import "@vlalg-nimbus/nb-inputs/dist/style.css";

export default defineNuxtPlugin(context => {
  context.vueApp.use(NbInputComponents)
})

To use, simply call the component, in this case it will be NbInputFile or nb-input-file.

Mode 1
<template>
  <NbInputFile />
</template>
Mode 2
<template>
  <nb-input-file />
</template>
Mode 3
<template>
  <nb-input-file></nb-input-file>
</template>

Preview & Playground

Select the component you want to edit/test.

Loading Sandbox...

Props

(*) = required.

Prop names below use camelCase in script; in templates use the usual kebab-case equivalents (e.g. input-name for inputName).

Identity, layout, and accessibility

nameTypeDefaultDescription
nbId (*)StringRoot wrapper id
inputName (*)Stringname on the file input and target for label for
displayString'b'b (block) or ib (inline-block) on the inner wrapper
inputWidthNumber200Base control width (px; styling may scale with breakpoints)
inputStyleString'background'background, line, or border
themeString'light'light or dark
textAlignString'left'left, center, or right for control text
sizeMediaQueryString'sm'xs, sm, md, or lg — responsive size token
tabindexString | Number0tabindex on the visible file control (role="button")
tabIndexNumber0Additional tab index prop (see component implementation)
ariaLabelString'Alternate Text Button'Fallback accessible name; combined with resolved chooseFileAriaLabel from locale where used
ariaAttrsObject{}Extra ARIA attributes merged onto the outer wrapper
titleString''title on the outer wrapper
disabledBooleanfalseDisables the control
inputReadonlyBooleanfalseRead-only behaviour / styling per NbInput patterns
requiredBooleanfalseSets required on the native file input
hasTabIndexEnterBooleantrueWhen true, Enter/Space on the focused visible control opens the picker
activeTextStyleString'normal'Active text style: normal, italic, or oblique
hasBorderRadiusBooleanfalseEnables themed border radius
borderRadiusNumber0.5Border radius (rem scale used in SCSS)
inputUppercaseBooleanfalseUppercase styling class on the control

File input behaviour

nameTypeDefaultDescription
inputPlaceholderString''Placeholder on the visible area when no files (theme may still set copy when empty)
multipleBooleanfalseMultiple files
fileExtensionString''accept string (comma-separated MIME/extensions, e.g. image/*, .pdf)
fileExtensionsString''Legacy alias used when fileExtension is empty
captureModeString'''', auto, user, or environment — sets capture on the input only when accept indicates media (image/*, video/*, etc.); ignored for non-media (e.g. PDF)
maxFileSizeBytesNumber5242880Max bytes per file (5 * 1024 * 1024 default)
maxFilesNumber | nullnullMax count when multiple; null = no limit
allowedExtensionsArray[]Extra allow-list, e.g. ['.png','.jpg']; applied in addition to accept when non-empty
allowDuplicatesBooleanfalseIf false, rejects same name + size in list or twice in one picker batch
gifWidthArray[]Allowed GIF widths (paired with gifHeight by index)
gifHeightArray[]Allowed GIF heights
videoRatioString''Target aspect, e.g. 16:9; empty skips ratio check
videoMaxDurationNumber | nullnullMax video duration in seconds; null skips
videoAspectToleranceNumber0.01Allowed deviation from target aspect ratio
showFilesCounterBooleantrueShow current/max when multiple and maxFiles set
showConstraintsTextBooleantrueShow summary line (limits / max size) under the control
showFileListBooleanfalseWhen false, hides the file-list region (default list and slot file-list). Selected files and changed / current-value are unchanged — only the list UI is suppressed
extraContendAbsoluteBooleanfalseControls whether .component__extra-content (constraints, message, file-list) is in normal flow (false, default) or position: absolute under the control (true). In templates: extra-contend-absolute. (Prop name keeps the Contend spelling.)
multipleFilesSelectedTextString'files selected'Suffix when multiple files are selected (count + this string)
blurOnDialogCancelBooleantrueWhen the user closes the picker without choosing, blur visible control so the label does not stay stuck “active”
localeObject{}Partial overrides for validation and UI strings. Defaults are listed under Locale keys; omitted keys keep the built-in English defaults shown there

Message block

nameTypeDefaultDescription
showMsgBooleanfalseShow the auxiliary message block (slot message / message prop)
hasMsgBooleanfalseWorks with showMsg to drive message visibility
messageString'Default message text'Default text inside the message slot
hasCustomMsgBooleanfalseApplies custom variant class on the message block
extraContendAbsoluteBooleanfalseControls whether .component__extra-content (constraints, message, file-list) is in normal flow (false, default) or position: absolute under the control (true). In templates: extra-contend-absolute. (Prop name keeps the Contend spelling.)

Label

nameTypeDefaultDescription
showLabelBooleanfalseShow floating label
labelString'Label text'Label text
labelBreakOnActiveBooleantrueLabel line-break behaviour when active
labelBackgroundString'transparent'Label background
labelPaddingString'1px 5px'Label padding
labelBorderRadiusNumber0Label border radius
labelLeftNumber5Label inset left (inactive)
labelRightNumber0Label inset right (inactive)
labelActiveLeftNumber5Label left when active
labelActiveRightNumber0Label right when active
labelActiveTopNumber-13Label vertical offset when active
inputLabelMarginActiveNumber15Extra margin when label is active
fontFamilyLabelString'Lato', sans-serifLabel font family
fontSizeLabelString'1em'Label font size (inactive)
fontSizeLabelActiveString'0.8em'Label font size when active
fontWeightLabelNumber400Label font weight
lightTextColorLabelString'#333333'Label color (light theme)
lightTextColorLabelActiveString'#333333'Active label color (light)
darkTextColorLabelString'#ffffff'Label color (dark theme)
darkTextColorLabelActiveString'#ffffff'Active label color (dark)

Icon area

nameTypeDefaultDescription
hasIconBooleanfalseShow icon slot region
iconDirectionString'left'left or right
iconWidthNumber32Icon area width
iconPaddingString'5px 10px'Icon padding (two tokens)
iconPaddingInputNumber35Input padding reserved for icon
iconMarginString'0'Icon outer margin
iconBorderRadiusNumber0Icon area radius
iconSizeNumber1Icon scale token
iconLightTextColorString'#f8f8f2'Icon text color (light)
iconDarkTextColorString'#f8f8f2'Icon text color (dark)
iconLightBgColorString'#353734'Icon background (light)
iconLightBgColorActiveString'#272936'Icon background active (light)
iconDarkBgColorString'#353734'Icon background (dark)
iconDarkBgColorActiveString'#272936'Icon background active (dark)
iconLightDisabledBgColorString'rgba(53, 55, 52, 0.3)'Icon disabled (light)
iconDarkDisabledBgColorString'rgba(68, 71, 90, 0.3)'Icon disabled (dark)

Main control typography and colors (light theme)

nameTypeDefaultDescription
fontFamilyString'Lato', sans-serifMain field font family
fontSizeStringnullMain field font size (theme fallback if null)
fontWeightNumber400Main field font weight
lightBgColorString'#f8f8f2'Field background (light)
lightBgColorFocusString'#eaeaea'Field background focused (light)
lightControlBorderColorString'#353734'Border / control outline (light)
lightControlBorderColorActiveString'#272936'Active border (light)
lightDisabledBgColorString'#dfdfd9'Disabled background (light)
lightTextColorString'#000000'Main text (light)
lightDisabledControlBorderColorString'rgba(53, 55, 52, 0.3)'Disabled border (light)
textColorString'#ffffff'Additional text color token used in bindings
caretColorString''Caret color when applicable
selectionBgColorString''Text selection background
selectionTextColorString''Text selection color

Main control colors (dark theme)

nameTypeDefaultDescription
darkBgColorString'#353734'Field background (dark)
darkBgColorFocusString'#272936'Field background focused (dark)
darkControlBorderColorString'#353734'Border (dark)
darkControlBorderColorActiveString'#272936'Active border (dark)
darkDisabledBgColorString'rgba(40, 42, 54, 1)'Disabled background (dark)
darkTextColorString'#000000'Main text (dark)
darkDisabledControlBorderColorString'rgba(68, 71, 90, 0.3)'Disabled border (dark)

Message block typography

nameTypeDefaultDescription
fontFamilyMsgString'Lato', sans-serifMessage block font
fontSizeMsgString'1em'Message block size
fontWeightMsgNumber400Message block weight
textMessageColorString'#f15574'Message text color

File list typography

nameTypeDefaultDescription
fontFamilyFileListString'Lato', sans-serifFile list font
fontSizeFileListString'1.6em'File list size
fontWeightFileListNumber400File list weight
textFileListColorString'#000000'File list text
textFileListPaddingString'2px'File list padding
textFileListMarginString'2px'File list margin

Constraints line typography

nameTypeDefaultDescription
fontFamilyConstraintsString'Lato', sans-serifConstraints font
fontSizeConstraintsString'1.2em'Constraints size
fontWeightConstraintsNumber400Constraints weight
textConstraintsColorString'#6b7280'Constraints text
textConstraintsPaddingString'0px'Constraints padding
textConstraintsMarginString'6px 0 0 0'Constraints margin

Clear action typography

nameTypeDefaultDescription
fontFamilyClearButtonString'Lato', sans-serifClear control font
fontSizeClearButtonString'1.2em'Clear control size
fontWeightClearButtonNumber400Clear control weight
textClearButtonColorString'#6b7280'Clear text
textClearButtonColorHoverString'#000000'Clear text hover
textClearButtonPaddingString'0px 6px'Clear padding
textClearButtonPaddingHoverString'0px 6px'Clear padding hover
textClearButtonBackgroundString'transparent'Clear background
textClearButtonBackgroundHoverString'transparent'Clear background hover

Counter typography

nameTypeDefaultDescription
fontFamilyCounterString'Lato', sans-serifCounter font
fontSizeCounterString'1.2em'Counter size
fontWeightCounterNumber600Counter weight
textCounterColorString'#374151'Counter text
textCounterPaddingString'0px'Counter padding
textCounterMarginString'4px 0px 0 0'Counter margin

Slots

namescopeDescription
iconIcon region when hasIcon is true
file-listfiles, removeFileSelected File[]; removeFile(index) removes one file (clears validation errors)
messageReplaces default message text when showMsg / hasMsg show the block

Events

Validation failures are not rendered inside NbInputFile. Listen to validation-errors (and optionally validation-error) in the parent and show messages there (list, toast, form summary, etc.).

namePayloadDescription
changedFile[]Current list after merge with a new selection
current-valueFile[]Same as changed
validation-error{ file: File | null, fileName: string, msg: string, errorType: string }Granular: fires once per problem (e.g. three bad files → three events). Best for side effects that should run per failure — see below.
validation-errorsArray<{ file, fileName, msg, errorType }>Snapshot: fires once per user action with the whole list of messages for that moment (or [] when errors are cleared). Best for binding UI state — see below.
focused / blurredEmitted when internal active label/control state toggles (focus proxy)
clickedEventWrapper click
file-dialog-closed{ reason: 'selected' | 'cancelled', selectedCount: number }After the native picker closes

Choosing validation-error vs validation-errors

Both carry the same shape per entry: { file, fileName, msg, errorType }. The difference is how often they fire and what you should do in the parent.

validation-errors (recommended for “what failed?” in the UI)

  • Emits one array after the component finishes processing a picker selection (merge + validation for that change event).
  • If the user picks three invalid files, you get one emission whose array has three items — easy to assign to a single ref / state field.
  • Also emits [] when errors are cleared (removed a file from the list, clear-all, etc.), so the parent stays in sync without guessing.
  • Use this for: an error list under the field, a summary alert, form-level validation state, or anything that should always mirror “the current set of validation messages” in one place.

validation-error (recommended for “something bad happened” reactions)

  • Emits separately for each rejected item (same example: three bad files → three events, one payload each).
  • The parent does not receive a [] “clear” signal from this event when errors are wiped; clearing is only reflected on validation-errors.
  • Use this for: toast per failure, analytics one event per failed file, focusing a specific row, or logging — anything tied to individual failures rather than replacing a full list.

Practical rule: bind validation-errorsfieldErrors = list for display. Optionally also listen to validation-errorshowToast(payload.msg) if you want instant feedback per file. You rarely need to only use validation-error and manually merge into an array unless you have custom queueing logic.

Error types (errorType)

Stable string on each validation payload (validation-error / entries inside validation-errors):

errorTypeWhen
TYPE_NOT_ALLOWEDMIME/type not in accept
EXTENSION_NOT_ALLOWEDExtension not in allowedExtensions
SIZE_EXCEEDEDFile larger than maxFileSizeBytes
DUPLICATE_IN_LISTDuplicate of an already selected file
DUPLICATE_IN_BATCHDuplicate within the same picker batch
GIF_DIMENSIONS_INVALID / GIF_LOAD_ERRORGIF rules / load
VIDEO_ASPECT_INVALID / VIDEO_DURATION_INVALID / VIDEO_LOAD_ERRORVideo rules / load
MAX_FILES_REACHEDmaxFiles limit (file may be null)
UNKNOWNUnexpected async validation failure

Locale keys

The locale prop is one object. Use it to override any of the following keys. Placeholders in templates use braces, e.g. {fileName} — they are substituted at runtime. Pass only the keys you need; any key you omit keeps the default English template in the right-hand column exactly as implemented in the component.

Validation message templates

These strings are shown when a file fails validation or when counters / limits apply.

keyPlaceholdersDefault template (English)
typeNotAllowed{fileType}, {accept}File type not allowed ({fileType}). Accept: {accept}
extensionNotAllowed{fileName}, {list}Extension not allowed for '{fileName}'. Allowed: {list}
sizeExceeded{fileName}File '{fileName}' exceeds maximum allowed size.
duplicateInList{fileName}File '{fileName}' is already added.
duplicateInBatch{fileName}Duplicate file '{fileName}' in the same selection.
gifDimensionsInvalid{width}, {height}, {allowedPairs}Invalid GIF dimensions: {width}x{height}. Allowed: {allowedPairs}
gifLoadErrorCould not load GIF for dimension validation
videoAspectInvalid{width}, {height}, {aspect}, {ratio}Invalid video aspect: {width}x{height} ({aspect}). Expected ratio {ratio}
videoDurationInvalid{duration}, {maxDuration}Invalid video duration: {duration}s. Max allowed: {maxDuration}s
videoLoadErrorCould not load video for validation
maxFilesReached{max}Limit of {max} files reached.
filesCounter{current}, {max}{current}/{max}

Component UI strings

These strings are used for the constraints line, clear control, visible control aria-label, and related copy.

keyPlaceholdersDefault (English)
clearActionClear
chooseFileAriaLabelChoose file
singleFileLimit1 file
multipleFilesLimitMultiple files
upToFilesLimit{max}Up to {max} files
maxSizePerFile{size}Max size per file: {size}

Example:

<NbInputFile
  :locale="{
    filesCounter: '{current} of {max}',
    sizeExceeded: 'File \'{fileName}\' is too large.',
    clearAction: 'Remove all'
  }"
/>

Behaviour notes

  • Opening the picker: click the wrapper, the visible control (role="button"), or the label (handled separately). Keyboard: Tab focuses the visible control; Enter/Space opens the dialog when hasTabIndexEnter is true. The native input covers the hit area with opacity 0. Keyboard focus may use :focus-visible so the label floats without breaking mouse clicks.
  • changed / current-value: emitted after validation and merge; invalid files do not enter the internal list (errors go to errors and validation events).
  • Clearing errors: a new picker run clears then re-validates; removing a file or using clear-all clears errors and emits validation-errors with [].
  • GIF / video: async validation in the browser; large files may add delay.
  • Remove in list: index-based; same file name with different size is treated as different files.

Example: errors in the parent

Show errors in the template from one reactive list, and optionally toast each failure:

<script setup>
import { ref } from 'vue'

const fieldErrors = ref([])

function onValidationErrors(list) {
  fieldErrors.value = Array.isArray(list) ? [...list] : []
}

function onValidationError(payload) {
  // Optional: one toast per bad file (does not replace fieldErrors)
  console.warn(payload.msg, payload.errorType, payload.fileName)
}
</script>

<template>
  <NbInputFile
    nb-id="pick"
    input-name="pick"
    @validation-errors="onValidationErrors"
    @validation-error="onValidationError"
  />
  <ul v-if="fieldErrors.length">
    <li v-for="(err, i) in fieldErrors" :key="i">
      {{ err.msg }} ({{ err.errorType }})
    </li>
  </ul>
</template>

Example: typical field

nbId and inputName are required. Listen to @changed or @current-value for the current File[] (there is no v-model).

<script setup>
import { ref } from 'vue'

const files = ref([])

function onFilesChanged(list) {
  files.value = Array.isArray(list) ? [...list] : []
}
</script>

<template>
  <NbInputFile
    nb-id="attachment"
    input-name="attachment"
    file-extension=".pdf,image/*"
    multiple
    :max-files="5"
    show-file-list
    show-constraints-text
    @changed="onFilesChanged"
  />
</template>

When the file field sits in a row with other controls and you do not want the constraints / message / file list to grow the row height, set extra-contend-absolute (kebab-case in templates):

<NbInputFile
  nb-id="attachment"
  input-name="attachment"
  show-file-list
  extra-contend-absolute
  @changed="onFilesChanged"
/>

Example: using File[] in the UI (complete)

File from @changed lives in memory. For inline preview in HTML/CSS, build a blob URL: URL.createObjectURL(file), then point src, srcset, href, or background-image at that string. Always URL.revokeObjectURL(url) when the file leaves the list or on unmount, or memory is not released.

Where blob URLs are commonly used

AreaTypical use
<img> / <picture>Thumbnails, lightboxes, srcset (often one URL per file unless you resize yourself)
<video controls>Preview before upload
<audio controls>Same for sound
<iframe>, <embed>, <object>PDF and other viewer-supported types (behaviour varies by browser)
<a download>“Download this selection” without hitting the server
window.open(url)Open preview in a new tab
CSSbackground-image: url(...) on a div (e.g. cover/crop UI)
CanvasImagedrawImage after img.src = objectURL (or createImageBitmap(file))
fetch(objectURL)Read back as blob() if you need to pipe data without the original File reference
UploadUse the File itself (see below) — not the blob: string

For very small files, FileReader.readAsDataURL is an alternative to object URLs (handy for data-URL-only APIs); it is less ideal for large video/PDF because the string is huge.

The example below uses @changed, revokes URLs on change/unmount, previews image / video / audio / PDF, offers a download link for any file, shows CSS background for the first image, and uploads with FormData.

<script setup>
import { ref, watch, onBeforeUnmount, computed } from 'vue'

const files = ref([])

/** One row per selected file: blob URL + metadata for templates */
const previews = ref([])

function fileKind(file) {
  const t = file.type || ''
  if (t.startsWith('image/')) return 'image'
  if (t.startsWith('video/')) return 'video'
  if (t.startsWith('audio/')) return 'audio'
  if (t === 'application/pdf' || /\.pdf$/i.test(file.name)) return 'pdf'
  return 'file'
}

function revokeAll() {
  previews.value.forEach((p) => URL.revokeObjectURL(p.url))
  previews.value = []
}

watch(
  files,
  (list) => {
    revokeAll()
    if (!list?.length) return
    previews.value = list.map((f) => ({
      url: URL.createObjectURL(f),
      name: f.name,
      type: f.type,
      kind: fileKind(f)
    }))
  },
  { deep: true }
)

onBeforeUnmount(revokeAll)

function onFilesChanged(list) {
  files.value = Array.isArray(list) ? [...list] : []
}

const firstImageUrl = computed(() => previews.value.find((p) => p.kind === 'image')?.url ?? '')

function openInNewTab(url) {
  window.open(url, '_blank', 'noopener,noreferrer')
}

async function uploadAll(endpoint) {
  const fd = new FormData()
  files.value.forEach((f, i) => fd.append(`file_${i}`, f, f.name))
  const res = await fetch(endpoint, { method: 'POST', body: fd })
  return res.ok
}
</script>

<template>
  <NbInputFile
    nb-id="media-pack"
    input-name="media-pack"
    file-extension="image/*,video/*,audio/*,.pdf"
    multiple
    show-file-list
    @changed="onFilesChanged"
  />

  <section v-if="previews.length" class="previews">
    <!-- CSS: background-image (first image only, example) -->
    <div
      v-if="firstImageUrl"
      class="hero-thumb"
      :style="{ backgroundImage: `url(${firstImageUrl})` }"
      role="img"
      :aria-label="'Cover: first image'"
    />

    <template v-for="p in previews" :key="p.url">
      <!-- Images: <picture> gives optional <source type="…">; <img> alone is fine too -->
      <picture v-if="p.kind === 'image'">
        <source :srcset="p.url" :type="p.type" />
        <img :src="p.url" :alt="p.name" width="100" height="100" style="object-fit: cover" />
      </picture>

      <video v-else-if="p.kind === 'video'" controls width="320" :src="p.url" />

      <audio v-else-if="p.kind === 'audio'" controls :src="p.url" />

      <!-- PDF: iframe works in many browsers; add sandbox if you must limit embedded content -->
      <iframe
        v-else-if="p.kind === 'pdf'"
        :title="p.name"
        width="100%"
        height="360"
        :src="p.url"
      />

      <!-- Other types: download link + open in new tab -->
      <p v-else>
        <a :href="p.url" :download="p.name">{{ p.name }}</a>
        <button type="button" @click="openInNewTab(p.url)">Open in new tab</button>
      </p>
    </template>
  </section>

  <!-- Upload: send File objects, not blob: strings -->
  <button type="button" :disabled="!files.length" @click="uploadAll('/api/upload')">
    Upload (example)
  </button>
</template>

<style scoped>
.hero-thumb {
  width: 200px;
  height: 120px;
  background-size: cover;
  background-position: center;
  border-radius: 8px;
  margin-bottom: 8px;
}
</style>

If you do not need <source>, a single <img :src="p.url"> for kind === 'image' is enough.

Canvas sketch (createImageBitmap avoids an intermediate object URL for drawing):

const bmp = await createImageBitmap(file)
ctx.drawImage(bmp, 0, 0)
bmp.close()

Limitations

  • No built-in drag-and-drop or HTTP upload; use File[] from @changed with your own upload logic.
  • Treat errorType values documented above as the stable contract for branching in your app; they are the strings emitted on validation payloads.