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 add @vlalg-nimbus/nb-inputs
Usage
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')
To use, simply call the component, in this case it will be NbInputFile or nb-input-file.
<template>
<NbInputFile />
</template>
Preview & Playground
Select the component you want to edit/test.
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
| name | Type | Default | Description |
|---|---|---|---|
| nbId (*) | String | — | Root wrapper id |
| inputName (*) | String | — | name on the file input and target for label for |
| display | String | 'b' | b (block) or ib (inline-block) on the inner wrapper |
| inputWidth | Number | 200 | Base control width (px; styling may scale with breakpoints) |
| inputStyle | String | 'background' | background, line, or border |
| theme | String | 'light' | light or dark |
| textAlign | String | 'left' | left, center, or right for control text |
| sizeMediaQuery | String | 'sm' | xs, sm, md, or lg — responsive size token |
| tabindex | String | Number | 0 | tabindex on the visible file control (role="button") |
| tabIndex | Number | 0 | Additional tab index prop (see component implementation) |
| ariaLabel | String | 'Alternate Text Button' | Fallback accessible name; combined with resolved chooseFileAriaLabel from locale where used |
| ariaAttrs | Object | {} | Extra ARIA attributes merged onto the outer wrapper |
| title | String | '' | title on the outer wrapper |
| disabled | Boolean | false | Disables the control |
| inputReadonly | Boolean | false | Read-only behaviour / styling per NbInput patterns |
| required | Boolean | false | Sets required on the native file input |
| hasTabIndexEnter | Boolean | true | When true, Enter/Space on the focused visible control opens the picker |
| activeTextStyle | String | 'normal' | Active text style: normal, italic, or oblique |
| hasBorderRadius | Boolean | false | Enables themed border radius |
| borderRadius | Number | 0.5 | Border radius (rem scale used in SCSS) |
| inputUppercase | Boolean | false | Uppercase styling class on the control |
File input behaviour
| name | Type | Default | Description |
|---|---|---|---|
| inputPlaceholder | String | '' | Placeholder on the visible area when no files (theme may still set copy when empty) |
| multiple | Boolean | false | Multiple files |
| fileExtension | String | '' | accept string (comma-separated MIME/extensions, e.g. image/*, .pdf) |
| fileExtensions | String | '' | Legacy alias used when fileExtension is empty |
| captureMode | String | '' | '', auto, user, or environment — sets capture on the input only when accept indicates media (image/*, video/*, etc.); ignored for non-media (e.g. PDF) |
| maxFileSizeBytes | Number | 5242880 | Max bytes per file (5 * 1024 * 1024 default) |
| maxFiles | Number | null | null | Max count when multiple; null = no limit |
| allowedExtensions | Array | [] | Extra allow-list, e.g. ['.png','.jpg']; applied in addition to accept when non-empty |
| allowDuplicates | Boolean | false | If false, rejects same name + size in list or twice in one picker batch |
| gifWidth | Array | [] | Allowed GIF widths (paired with gifHeight by index) |
| gifHeight | Array | [] | Allowed GIF heights |
| videoRatio | String | '' | Target aspect, e.g. 16:9; empty skips ratio check |
| videoMaxDuration | Number | null | null | Max video duration in seconds; null skips |
| videoAspectTolerance | Number | 0.01 | Allowed deviation from target aspect ratio |
| showFilesCounter | Boolean | true | Show current/max when multiple and maxFiles set |
| showConstraintsText | Boolean | true | Show summary line (limits / max size) under the control |
| showFileList | Boolean | false | When 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 |
| extraContendAbsolute | Boolean | false | Controls 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.) |
| multipleFilesSelectedText | String | 'files selected' | Suffix when multiple files are selected (count + this string) |
| blurOnDialogCancel | Boolean | true | When the user closes the picker without choosing, blur visible control so the label does not stay stuck “active” |
| locale | Object | {} | 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
| name | Type | Default | Description |
|---|---|---|---|
| showMsg | Boolean | false | Show the auxiliary message block (slot message / message prop) |
| hasMsg | Boolean | false | Works with showMsg to drive message visibility |
| message | String | 'Default message text' | Default text inside the message slot |
| hasCustomMsg | Boolean | false | Applies custom variant class on the message block |
| extraContendAbsolute | Boolean | false | Controls 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
| name | Type | Default | Description |
|---|---|---|---|
| showLabel | Boolean | false | Show floating label |
| label | String | 'Label text' | Label text |
| labelBreakOnActive | Boolean | true | Label line-break behaviour when active |
| labelBackground | String | 'transparent' | Label background |
| labelPadding | String | '1px 5px' | Label padding |
| labelBorderRadius | Number | 0 | Label border radius |
| labelLeft | Number | 5 | Label inset left (inactive) |
| labelRight | Number | 0 | Label inset right (inactive) |
| labelActiveLeft | Number | 5 | Label left when active |
| labelActiveRight | Number | 0 | Label right when active |
| labelActiveTop | Number | -13 | Label vertical offset when active |
| inputLabelMarginActive | Number | 15 | Extra margin when label is active |
| fontFamilyLabel | String | 'Lato', sans-serif | Label font family |
| fontSizeLabel | String | '1em' | Label font size (inactive) |
| fontSizeLabelActive | String | '0.8em' | Label font size when active |
| fontWeightLabel | Number | 400 | Label font weight |
| lightTextColorLabel | String | '#333333' | Label color (light theme) |
| lightTextColorLabelActive | String | '#333333' | Active label color (light) |
| darkTextColorLabel | String | '#ffffff' | Label color (dark theme) |
| darkTextColorLabelActive | String | '#ffffff' | Active label color (dark) |
Icon area
| name | Type | Default | Description |
|---|---|---|---|
| hasIcon | Boolean | false | Show icon slot region |
| iconDirection | String | 'left' | left or right |
| iconWidth | Number | 32 | Icon area width |
| iconPadding | String | '5px 10px' | Icon padding (two tokens) |
| iconPaddingInput | Number | 35 | Input padding reserved for icon |
| iconMargin | String | '0' | Icon outer margin |
| iconBorderRadius | Number | 0 | Icon area radius |
| iconSize | Number | 1 | Icon scale token |
| iconLightTextColor | String | '#f8f8f2' | Icon text color (light) |
| iconDarkTextColor | String | '#f8f8f2' | Icon text color (dark) |
| iconLightBgColor | String | '#353734' | Icon background (light) |
| iconLightBgColorActive | String | '#272936' | Icon background active (light) |
| iconDarkBgColor | String | '#353734' | Icon background (dark) |
| iconDarkBgColorActive | String | '#272936' | Icon background active (dark) |
| iconLightDisabledBgColor | String | 'rgba(53, 55, 52, 0.3)' | Icon disabled (light) |
| iconDarkDisabledBgColor | String | 'rgba(68, 71, 90, 0.3)' | Icon disabled (dark) |
Main control typography and colors (light theme)
| name | Type | Default | Description |
|---|---|---|---|
| fontFamily | String | 'Lato', sans-serif | Main field font family |
| fontSize | String | null | Main field font size (theme fallback if null) |
| fontWeight | Number | 400 | Main field font weight |
| lightBgColor | String | '#f8f8f2' | Field background (light) |
| lightBgColorFocus | String | '#eaeaea' | Field background focused (light) |
| lightControlBorderColor | String | '#353734' | Border / control outline (light) |
| lightControlBorderColorActive | String | '#272936' | Active border (light) |
| lightDisabledBgColor | String | '#dfdfd9' | Disabled background (light) |
| lightTextColor | String | '#000000' | Main text (light) |
| lightDisabledControlBorderColor | String | 'rgba(53, 55, 52, 0.3)' | Disabled border (light) |
| textColor | String | '#ffffff' | Additional text color token used in bindings |
| caretColor | String | '' | Caret color when applicable |
| selectionBgColor | String | '' | Text selection background |
| selectionTextColor | String | '' | Text selection color |
Main control colors (dark theme)
| name | Type | Default | Description |
|---|---|---|---|
| darkBgColor | String | '#353734' | Field background (dark) |
| darkBgColorFocus | String | '#272936' | Field background focused (dark) |
| darkControlBorderColor | String | '#353734' | Border (dark) |
| darkControlBorderColorActive | String | '#272936' | Active border (dark) |
| darkDisabledBgColor | String | 'rgba(40, 42, 54, 1)' | Disabled background (dark) |
| darkTextColor | String | '#000000' | Main text (dark) |
| darkDisabledControlBorderColor | String | 'rgba(68, 71, 90, 0.3)' | Disabled border (dark) |
Message block typography
| name | Type | Default | Description |
|---|---|---|---|
| fontFamilyMsg | String | 'Lato', sans-serif | Message block font |
| fontSizeMsg | String | '1em' | Message block size |
| fontWeightMsg | Number | 400 | Message block weight |
| textMessageColor | String | '#f15574' | Message text color |
File list typography
| name | Type | Default | Description |
|---|---|---|---|
| fontFamilyFileList | String | 'Lato', sans-serif | File list font |
| fontSizeFileList | String | '1.6em' | File list size |
| fontWeightFileList | Number | 400 | File list weight |
| textFileListColor | String | '#000000' | File list text |
| textFileListPadding | String | '2px' | File list padding |
| textFileListMargin | String | '2px' | File list margin |
Constraints line typography
| name | Type | Default | Description |
|---|---|---|---|
| fontFamilyConstraints | String | 'Lato', sans-serif | Constraints font |
| fontSizeConstraints | String | '1.2em' | Constraints size |
| fontWeightConstraints | Number | 400 | Constraints weight |
| textConstraintsColor | String | '#6b7280' | Constraints text |
| textConstraintsPadding | String | '0px' | Constraints padding |
| textConstraintsMargin | String | '6px 0 0 0' | Constraints margin |
Clear action typography
| name | Type | Default | Description |
|---|---|---|---|
| fontFamilyClearButton | String | 'Lato', sans-serif | Clear control font |
| fontSizeClearButton | String | '1.2em' | Clear control size |
| fontWeightClearButton | Number | 400 | Clear control weight |
| textClearButtonColor | String | '#6b7280' | Clear text |
| textClearButtonColorHover | String | '#000000' | Clear text hover |
| textClearButtonPadding | String | '0px 6px' | Clear padding |
| textClearButtonPaddingHover | String | '0px 6px' | Clear padding hover |
| textClearButtonBackground | String | 'transparent' | Clear background |
| textClearButtonBackgroundHover | String | 'transparent' | Clear background hover |
Counter typography
| name | Type | Default | Description |
|---|---|---|---|
| fontFamilyCounter | String | 'Lato', sans-serif | Counter font |
| fontSizeCounter | String | '1.2em' | Counter size |
| fontWeightCounter | Number | 600 | Counter weight |
| textCounterColor | String | '#374151' | Counter text |
| textCounterPadding | String | '0px' | Counter padding |
| textCounterMargin | String | '4px 0px 0 0' | Counter margin |
Slots
| name | scope | Description |
|---|---|---|
| icon | — | Icon region when hasIcon is true |
| file-list | files, removeFile | Selected File[]; removeFile(index) removes one file (clears validation errors) |
| message | — | Replaces 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.).
| name | Payload | Description |
|---|---|---|
| changed | File[] | Current list after merge with a new selection |
| current-value | File[] | 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-errors | Array<{ 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 / blurred | — | Emitted when internal active label/control state toggles (focus proxy) |
| clicked | Event | Wrapper 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
changeevent). - 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 onvalidation-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-errors → fieldErrors = list for display. Optionally also listen to validation-error → showToast(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):
errorType | When |
|---|---|
TYPE_NOT_ALLOWED | MIME/type not in accept |
EXTENSION_NOT_ALLOWED | Extension not in allowedExtensions |
SIZE_EXCEEDED | File larger than maxFileSizeBytes |
DUPLICATE_IN_LIST | Duplicate of an already selected file |
DUPLICATE_IN_BATCH | Duplicate within the same picker batch |
GIF_DIMENSIONS_INVALID / GIF_LOAD_ERROR | GIF rules / load |
VIDEO_ASPECT_INVALID / VIDEO_DURATION_INVALID / VIDEO_LOAD_ERROR | Video rules / load |
MAX_FILES_REACHED | maxFiles limit (file may be null) |
UNKNOWN | Unexpected 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.
| key | Placeholders | Default 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} |
| gifLoadError | — | Could 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 |
| videoLoadError | — | Could 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.
| key | Placeholders | Default (English) |
|---|---|---|
| clearAction | — | Clear |
| chooseFileAriaLabel | — | Choose file |
| singleFileLimit | — | 1 file |
| multipleFilesLimit | — | Multiple 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 whenhasTabIndexEnteris true. The native input covers the hit area with opacity 0. Keyboard focus may use:focus-visibleso 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 toerrorsand validation events).- Clearing errors: a new picker run clears then re-validates; removing a file or using clear-all clears errors and emits
validation-errorswith[]. - GIF / video: async validation in the browser; large files may add delay.
- Remove in list: index-based; same file name with different
sizeis 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
| Area | Typical 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 |
| CSS | background-image: url(...) on a div (e.g. cover/crop UI) |
| Canvas | Image → drawImage after img.src = objectURL (or createImageBitmap(file)) |
fetch(objectURL) | Read back as blob() if you need to pipe data without the original File reference |
| Upload | Use 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@changedwith your own upload logic. - Treat
errorTypevalues documented above as the stable contract for branching in your app; they are the strings emitted on validation payloads.
