File input
File input — choosing a file from the device.
Examples
<div class="field" data-ui="file-input" data-file-max="2097152">
<span class="field-label" id="attach-label">Attachments</span>
<label class="file-drop">
<svg class="icon icon-lg file-drop-icon" aria-hidden="true"><use href="#icon-upload"></use></svg>
<span class="file-drop-title">Drop files here, or choose them</span>
<span>PNG, JPEG or PDF, up to 2 MB each</span>
<input class="input file-input sr-only" id="attach" name="attach" type="file" multiple
accept="image/png,image/jpeg,application/pdf"
aria-labelledby="attach-label" aria-describedby="attach-error" />
</label>
<ul class="file-list" data-file-list></ul>
<p class="field-error" id="attach-error" data-file-error hidden></p>
</div>The zone is a <label for> and not a <div> with a click handler, which is what keeps this small: clicking it opens the picker with no script, it is announced as part of the control, and the <input> inside it is still the thing that focuses and submits.
That input is .sr-only, never display: none — a hidden file input is not focusable and drops out of the tab order, which is the trap this component has warned about since it was CSS-only. Removing a file is the part with no shortcut: input.files is a read-only live FileList with no remove() in any engine, so both accepting a drop and dropping a row go through a fresh DataTransfer whose files is assigned back. Thumbnails are URL.createObjectURL, and every one of them is revoked when its row goes — forgetting is a leak that only shows after somebody attaches a hundred photographs. data-file-max is a courtesy and not a control: the server has to check anyway.
PNG or JPEG, up to 2 MB.
<div class="field">
<label class="field-label" for="avatar">Profile picture</label>
<input class="input file-input" id="avatar" name="avatar" type="file"
accept="image/png,image/jpeg" aria-describedby="avatar-help" />
<p class="field-help" id="avatar-help">PNG or JPEG, up to 2 MB.</p>
</div>
<div class="field">
<label class="field-label" for="avatar-quiet">Quiet, for a bordered container</label>
<input class="input file-input file-input-quiet" id="avatar-quiet" name="avatar-quiet"
type="file" accept="image/png,image/jpeg" />
</div>Without the module this is the whole component, and it is complete: worn with .input, which owns the box, and adding exactly one thing — ::file-selector-button, the standard way to reach the button the browser draws inside the control.
That pseudo-element is what makes the old trick of hiding the input behind a <label class="btn"> unnecessary, and it was never a good trade: a hidden input is not focusable, so the control leaves the tab order and the file name the UA renders disappears, which is why every implementation of it ends up writing script to put both back.
Tokens 26
Level 2, declared on .file-input itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --file-drop-active-bg | var(--color-surface-raised) |
| --file-drop-active-border-color | var(--color-primary) |
| --file-drop-bg | var(--color-surface-sunken) |
| --file-drop-border-color | var(--color-border-strong) |
| --file-drop-color | var(--color-on-surface-muted) |
| --file-drop-gap | var(--space-2) |
| --file-drop-icon-color | var(--color-on-surface-subtle) |
| --file-drop-padding | var(--space-8) |
| --file-drop-radius | var(--radius-box) |
| --file-item-bg | var(--color-surface-raised) |
| --file-item-border-color | var(--color-border-subtle) |
| --file-item-gap | var(--space-3) |
| --file-size-color | var(--color-on-surface-muted) |
| --file-thumb-radius | var(--radius-md) |
| --file-thumb-size | 2.75rem |
| --input-padding-inline | var(--input-padding-block) |
| --file-input-button-bg | var(--color-secondary) |
| --file-input-button-border-color | var(--color-secondary) |
| --file-input-button-color | var(--color-on-secondary) |
| --file-input-button-font-size | var(--font-size-sm) |
| --file-input-button-font-weight | var(--font-weight-medium) |
| --file-input-button-gap | var(--space-3) |
| --file-input-button-hover-bg | var(--color-secondary-hover) |
| --file-input-button-padding-block | var(--space-2) |
| --file-input-button-padding-inline | var(--space-3) |
| --file-input-button-radius | var(--radius-field) |
Variants and states
Variants
None. It is one shape, and the page does the rest.
Inside it
.file-drop.file-input-quiet.file-drop-icon.file-drop-title.file-item.file-list.file-meta.file-name.file-remove.file-size.file-thumb
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:disabled:focus:hoverdata-dropping
Before you ship it
This one needs a script. Opt the markup in with data-ui="file-input" and import import 'mostlycss/js/components/file-input' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.
What you have to do 1 requirements
- Give it a real
<label>, associated withfor. The button's own text ("Choose File", "Browse…") is supplied by the UA, is not the accessible name of the control, and is localised out from under you. Where a size limit or an accepted format matters, say it in.field-helpand pointaria-describedbyat it.acceptis silent to a screen reader.
src/css/components/file-input.css · npx mostlycss add file-input