J3W1 / DESIGN SYSTEM
File input
A native file input behind a label styled as a secondary button, optionally inside a drop zone, with a list of chosen files and per-file errors.
stable · LIGHT DOM · 1.1.0
Light DOM retains static content, native semantics, label relationships and host composition without duplicating controls.
Live variants
default
Choose local files. This component does not upload them; the host validates its own file policy.
<j3w1-file-input id="example-file-input-default"><div class="file-input">
<label class="file-input-label" for="example-file-input-default-fi-attach">Attachments</label>
<div class="file-input-root">
<input class="file-input-native" id="example-file-input-default-fi-attach" type="file" multiple="" aria-describedby="example-file-input-default-fi-attach-help example-file-input-default-fi-attach-message" name="example-file-input-default-fi-attach">
<label class="file-input-button" for="example-file-input-default-fi-attach">Choose files</label>
<span class="file-input-status" role="status">No file chosen</span>
<span class="file-input-drop-hint" aria-hidden="true">Release to add</span>
</div>
<div class="file-input-progress" aria-hidden="true"></div>
<p class="file-input-help" id="example-file-input-default-fi-attach-help">Choose local files. This component does not upload them; the host validates its own file policy.</p>
<p class="file-input-message" id="example-file-input-default-fi-attach-message">✕ Choose at least one file.</p>
</div></j3w1-file-input>drop-zone
Choose local files. This component does not upload them; the host validates its own file policy.
<j3w1-file-input id="example-file-input-drop-zone"><div class="file-input">
<label class="file-input-label" for="example-file-input-drop-zone-fi-zone">Screenshots</label>
<div class="file-input-root file-input-zone">
<input class="file-input-native" id="example-file-input-drop-zone-fi-zone" type="file" multiple="" accept="image/png,image/webp" aria-describedby="example-file-input-drop-zone-fi-zone-help example-file-input-drop-zone-fi-zone-message" name="example-file-input-drop-zone-fi-zone">
<label class="file-input-button" for="example-file-input-drop-zone-fi-zone">Choose files</label>
<span class="file-input-status" role="status">No file chosen</span>
<span class="file-input-drop-hint" aria-hidden="true">Release to add</span>
</div>
<div class="file-input-progress" aria-hidden="true"></div>
<p class="file-input-help" id="example-file-input-drop-zone-fi-zone-help">Choose local files. This component does not upload them; the host validates its own file policy.</p>
<p class="file-input-message" id="example-file-input-drop-zone-fi-zone-message">✕ Only PNG or WebP files.</p>
</div></j3w1-file-input>with-list
Choose local files. This component does not upload them; the host validates its own file policy.
<j3w1-file-input id="example-file-input-with-list"><div class="file-input">
<label class="file-input-label" for="example-file-input-with-list-fi-list">Attachments</label>
<div class="file-input-root">
<input class="file-input-native" id="example-file-input-with-list-fi-list" type="file" multiple="" aria-describedby="example-file-input-with-list-fi-list-help example-file-input-with-list-fi-list-message" name="example-file-input-with-list-fi-list">
<label class="file-input-button" for="example-file-input-with-list-fi-list">Add files</label>
<span class="file-input-status" role="status">No file chosen</span>
<span class="file-input-drop-hint" aria-hidden="true">Release to add</span>
</div>
<div class="file-input-progress" aria-hidden="true"></div>
<ul class="file-input-list" aria-label="Chosen files"></ul>
<p class="file-input-help" id="example-file-input-with-list-fi-list-help">Choose local files. This component does not upload them; the host validates its own file policy.</p>
<p class="file-input-message" id="example-file-input-with-list-fi-list-message">✕ One file could not be added.</p>
</div></j3w1-file-input>Install and register
Use the exact version. If npm publication is pending, install the release tarball from the installation guide.
npm install --save-exact @j3w1/ui@1.1.0import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/file-input.css';
import '@j3w1/ui/register/file-input';Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.
Copy the complete implementation
npx --package=@j3w1/ui@1.1.0 j3w1-ui copy file-input --out ./vendor/j3w1/file-inputThe CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.
Public API
attributes
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
files | File[] | Current native files; files are never uploaded by the component. Read only. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
clear | (): void | Clear the current native file selection. |
events
| Name | Type / signature | Meaning |
|---|---|---|
j3w1-change | CustomEvent | Native file selection changed; read files from the component. |
Dependencies and verification
Registered components: none. Style dependencies: none.
Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.
Full implementation manifest, API and token dependency closure
Canonical specification
Purpose
Choosing one or more files from the host. The native input keeps the host chooser, its keyboard behaviour and its security model; the theme replaces only the input’s own rendering with a button-styled label, adds an optional drop zone, and lists what was chosen with any per-file error.
Anatomy
The field label; the root row (or the drop zone) holding the button-styled
label and the status text; the hidden-but-focusable native input; an optional
list of chosen files, each with name, size in color.text.subtle and a
remove button, one of which may carry the ✕ glyph and an error in
color.status.danger.text; a static striped progress bar while uploading;
help and validation message below.
States
| State | Visual | Non-colour channel |
|---|---|---|
| default | button: 1px color.action.secondary.border outline, text color.action.secondary.text; zone: 1px dashed color.interaction.drop-target | — |
| hover | button background → color.action.secondary.hover-bg; border unchanged | cursor: pointer |
| focus-visible | ring 1px dashed color.interaction.focus.ring at −2px on the button (the input has focus) | the ring |
| drop-target | root fill color.interaction.marquee; border 1px dashed color.interaction.drop-target | dashed pattern; the hint text changes to “Release to add” |
| invalid | button border 2px color.status.danger.border; message with the ✕ glyph; a failed row shows its own ✕ | border width 1 → 2px; glyphs; aria-invalid |
| disabled | button text color.text.disabled, border color.border.disabled, background color.interaction.disabled.bg; the zone’s dashes take color.border.disabled; no hover | disabled; cursor: not-allowed |
| loading | a static bar of 45° stripes, color.action.primary.bg on color.interaction.pressed.bg, below the row; the button stays enabled | aria-busy; the status text reads “Uploading n files”; no animation |
Precedence when several apply: disabled > loading > invalid > drop-target > hover; focus-visible is always drawn.
Keyboard
Tab reaches the native input, which is drawn as the button; Enter or Space opens the host chooser; Escape in the chooser returns to the input. Each remove button in the list is its own tab stop after the input, and Delete or Backspace on it acts like Enter. Dropping is a pointer gesture only and never the sole way to add a file.
Accessibility
The native input is hidden with the sr-only pattern, never display: none
or visibility: hidden, so it keeps focus and the host chooser. It is named
by both labels (“Attachments” and “Choose files”), and aria-describedby
lists help and message. The status text and the list are announced through a
polite live region after a choice; per-file errors are in the row text, not
only in colour. The drop zone has no role and no tabindex; aria-busy on
the root marks uploading. Contrast: button text 10.37:1 on the canvas and
6.84:1 on the hover fill, button border 4.45:1, drop-target border 4.69:1 on
the canvas and 4.52:1 on the marquee fill, status and help 5.81:1, sizes
5.10:1, danger text 5.40:1.
Portability
The button is the secondary button of the actions family; the zone is a dashed border and a translucent fill; the stripes are a repeating gradient (or a hatched brush). Toolkits without drag-and-drop omit the zone and keep the button; toolkits without a translucent fill composite the marquee over the canvas and record the value.
Non-examples
A hidden input with display: none and a button that clicks it by script. A
drop zone that is the only way to add files. A rounded, dotted, animated
“pulsing” zone. A spinner while uploading. A progress bar that animates its
stripes. Errors shown by a red file name alone. Disabling by opacity.