Input file
Input file is a field for picking one or more files, by click or drag-and-drop, through a native picker wrapped in a custom dropzone. It can show selected files as a thumbnail grid or as a row list with an icon, name, and size. Each file can be removed on its own, all at once, or as part of a checked selection, and clicking a file opens it in a new tab.
Dependencies
When to use
Reach for an input file whenever someone needs to attach one or more
files to a form, such as a profile picture, a set of product photos, a
signed PDF, or supporting documents for a claim. It shows files as
thumbnails by default, better suited to images, or as a compact row list
for anything else, such as PDFs, spreadsheets, or other attachments. For
a single free-text value, reach for
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/input-file.js";
<eun-input-file name="photos" label="Photos" multiple></eun-input-file>
Importing the file registers <eun-input-file> as a custom element, with no further setup
needed. It works with any framework, or none, since it's a standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/input-file.js";
</script>
<eun-input-file name="photos" label="Photos" multiple></eun-input-file>
npm install @eunomia/elements
import "@eunomia/elements/input-file.js";
function PhotosField() {
return <eun-input-file name="photos" label="Photos" multiple />;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/input-file.js";
export function PhotosField() {
return <eun-input-file name="photos" label="Photos" multiple />;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/input-file.js";
</script>
<template>
<eun-input-file name="photos" label="Photos" multiple />
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/input-file.js";
@Component({
selector: "app-photos-field",
template: `<eun-input-file name="photos" label="Photos" multiple>
</eun-input-file>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class PhotosFieldComponent {}
Alternatives
eun-inputGuidance
- Set
kind="file"whenever the accepted content isn't images: a row list with a name and size reads far better than a thumbnail grid for PDFs or documents - Set
max-sizewhenever there's a real backend limit, so oversized files are rejected client-side instead of failing on submit - Only turn on
selectablewhen someone plausibly needs to remove several files at once: for a handful of files, the per-item remove button is already enough
- Using this component for a single free-text value: see Alternatives above
- Relying on
accept/max-sizealone as a security boundary: always re-validate file type and size server-side - Overriding colors with inline styles instead of the
--input-file-*CSS variables
Live testing
Properties
Input file <eun-input-file>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| max-size | number | undefined | — | Maximum size, in bytes, accepted per file. |
| max-files | number | undefined | — | Maximum total number of files accepted. Only relevant with `multiple`. |
| drop-label | string | 'Drag and drop files here, or' | Static text displayed in the dropzone |
| browse-label | string | 'browse files' | The visible text of the dropzone's browse link |
| aria-label-remove | string | 'Remove file' | The accessible label of each item's remove button |
| aria-label-select | string | 'Select file' | The accessible label prefix of each item's checkbox |
| remove-all-label | string | 'Remove all' | The label of the always-available bulk clear action |
| select-all-label | string | 'Select all' | The label of the selection toolbar's select-all checkbox |
| delete-selected-label | string | 'Delete selected' | The label of the selection toolbar's bulk delete button |
| aria-label-open | string | 'Open in a new tab' | Appended to each item's accessible name |
| kind | 'image' | 'file' | 'image' | The accepted content kind: image thumbnails or generic file rows |
| accept | string | — | The native accept filter, forwarded to the picker and used to filter drops. Defaults to images when kind is image, unset otherwise |
| multiple | boolean | false | Whether more than one file can be selected |
| selectable | boolean | false | Shows per-item checkboxes, a select-all toggle, and a bulk delete action once more than one file is present |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| label | string | — | The title label of the field, rendered through the label component |
| instructions | string | — | Instructions displayed below the label |
| name | string | — | The field name |
| value | File[] | — | The currently selected files |
| disabled | boolean | false | Whether the field is disabled |
| readonly | boolean | false | Whether the field is read only |
| required | boolean | false | Whether at least one file is required |
| hideError | boolean | false | Whether the errors are hidden |
| validators | Array<Validators<File[]>> | — | The list of validation rules applied to the field value |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired whenever the selected files change |
| eunfilereject | FileRejectEvent | Fired whenever one or more offered files are rejected, for the wrong type or exceeding the size or count limit |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --input-file-border-color | Sets the border color of the dropzone |
| --input-file-border-color-hover | Sets the border color on hover |
| --input-file-border-color-focus | Sets the border color on focus |
| --input-file-border-radius | Sets the corner radius of the dropzone and items |
| --input-file-background | Sets the background color of the dropzone |
| --input-file-background-hover | Sets the background color on hover or drag over |
| --input-file-text-color | Sets the text color |
| --input-file-icon-color | Sets the color of icons and action buttons |
| --input-file-error-color | Sets the color of the error message |
| --input-file-hint-color | Sets the color of the hint message |
| --input-file-focus-outline-color | Sets the color of the focus outline |
| --input-file-item-background | Sets the background color of each item |
| --input-file-item-border-color | Sets the border color of each item |
| --input-file-gap | Sets the gap between the dropzone, header, and items |
| --input-file-thumbnail-size | Sets the minimum size of image thumbnails |
Basic
Image kind (default)
<eun-input-file name="avatar" label="Avatar" hint="PNG or JPG"></eun-input-file>
File kind
<eun-input-file
name="document"
label="Document"
kind="file"
accept="application/pdf"
>
</eun-input-file>
Multiple
With multiple, the dropzone stays visible above the item list/grid so more files can keep
being added.
<eun-input-file name="gallery" label="Gallery" multiple></eun-input-file>
Selectable, with bulk delete
selectable adds a checkbox to each item, plus a select-all checkbox and a "Delete
selected" action once at least one is checked, independent from the always-available
"Remove all".
<eun-input-file
name="attachments"
label="Attachments"
kind="file"
multiple
selectable
>
</eun-input-file>
Restricting size and count
max-size (bytes) rejects oversized files, while max-files caps the
total count once multiple is set. Both emit eunfilereject instead of
silently dropping the file, so listen for it to surface why a file
didn't make it in.
No rejections yet.
<eun-input-file
name="receipts"
label="Receipts"
kind="file"
multiple
max-size="2000000"
max-files="3"
hint="Up to 3 files, 2 MB each"
>
</eun-input-file>
const field = document.querySelector("#receipts");
field.addEventListener("eunfilereject", (event) => {
const summary = event.files
.map(({ file, reason }) => `${file.name}: ${reason}`)
.join("\n");
console.log(summary);
});
States
Disabled
<eun-input-file label="Disabled" disabled></eun-input-file>
Readonly
A readonly field still shows and submits its files, but nothing can be added or removed.
<eun-input-file label="Readonly" readonly></eun-input-file>
Required
<eun-input-file
label="Proof of address"
kind="file"
required
hint="A recent utility bill or bank statement"
>
</eun-input-file>
Forms
Like every other Eunomia field, eun-input-file is a real
File objects themselves are
submitted under its name, exactly like a native <input type="file">.
[]
<form id="my-form">
<eun-input-file name="attachments" label="Attachments" kind="file" multiple>
</eun-input-file>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</form>
const form = document.querySelector("#my-form");
form.addEventListener("eunchange", () => {
const files = new FormData(form).getAll("attachments");
console.log(files.map((file) => file.name));
});
Custom
Override the --input-file-* CSS variables, listed in full in the API tab.
<eun-input-file class="custom-input-file" label="Custom"></eun-input-file>
.custom-input-file {
--input-file-border-color: #be185d;
--input-file-border-color-hover: #be185d;
--input-file-border-color-focus: #be185d;
--input-file-focus-outline-color: #be185d;
}
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus in/out of the dropzone (or the first item, once a file already fills its place), then through each item and its own remove/checkbox buttons |
Enter / Space on the dropzone |
Opens the native OS file picker, natively |
Enter / Space on an item |
Opens that file in a new tab |
Delete / Backspace on an item |
Removes that file directly, without needing to reach its own remove button |
Enter / Space on a remove/checkbox/action button |
Activates that button, natively |
Drag-and-drop is a pointer-only shortcut for the exact same selection the native picker
already provides through the dropzone's Enter/Space, so nothing here is drag-and-drop-only.
Aria attributes
aria-invalidis not set directly (no native constraint beyondrequired, itself surfaced throughcheckValidity()/validitylike every other field here)aria-labelon the native, visually-hidden file field: set from thelabelproperty directly, for the same cross-shadow-boundary reason documented oneun-input's own Accessibility tab (a plain-textaria-labelinstead ofaria-labelledby/forpointing at the rendered<eun-label>)aria-describedby="description": set on the native field whenever a hint or error message is actually rendered below itaria-disabledon the dropzone: reflects bothdisabledand "max-filesalready reached"- Each item is
role="button"with anaria-labelcombining the file name and a short "Open in a new tab" hint (aria-label-open), so its accessible name and its primary action always match - Each item's remove button and (when
selectable) checkbox carry their ownaria-labelincluding the file name, so multiple items' controls stay distinguishable to assistive technology - The file count above the items (
"3 files") isaria-live="polite", so additions and removals are announced without moving focus
Label accessibility
Always set label (or slot content into the label slot): describe what the files are
for ("Proof of address", "Product photos"), not just that a file is expected.
Disabled vs. readonly
disabled removes the field from the tab order (tabindex="-1", aria-disabled="true"),
disables every action, and excludes the value from form submission entirely. readonly
keeps the field and its items focusable and their values submitted, but blocks adding,
removing, or reordering, communicating "you can't change this right now", not "this
doesn't apply". Prefer readonly over disabled whenever the already-selected files are
still meaningful context for the user.
Required field
required behaves like a native required <input type="file">: an empty selection is
invalid, any non-empty selection is valid, regardless of multiple.
Rejections aren't validation errors
Files rejected for type/size/max-files never reach value in the first place, so there's
nothing invalid to report through checkValidity(). A native file picker behaves the same
way, since you simply can't pick a mismatched file. Listen for eunfilereject if you want to
surface why a specific file didn't make it in (see the Examples tab).
Reference links