File Upload
The <x-wirekit:: component is a drag-and-drop file dropzone with click-to-browse fallback. It displays a live list of selected files with their sizes, written in your application's locale, the unit included (1,5 MB on a German page, 1,5 Mo on a French one).
Usage
Max 5 MB — PDF, DOC, or DOCX
Accepted File Types
The accept prop is passed straight through to the native <input type="file" accept="..."> attribute. The browser uses it as a hint in the file picker (and filters the "All files" vs "specific types" dropdown) — it is NOT a security boundary. A user can always bypass it via drag-and-drop or by choosing "All files". You MUST validate file types server-side in your Laravel validator (e.g. mimes:pdf,docx or mimetypes:application/).
Three ways to specify accepted types
You can mix any of the following inside a single comma-separated string:
- File extensions — start with a dot:
.pdf,.docx,.csv - MIME types — full type string:
application/pdf,image/png - Wildcard groups — generic category:
image/*,video/*,audio/*
Extensions are the friendliest because they match what users see in their file manager, but MIME types and wildcards are more precise and work better on mobile devices.
Common recipes
| Use case | accept value |
|---|---|
| Any image | image/* |
| PNG or JPG only | image/png,image/jpeg or .png,.jpg,.jpeg |
| PDF only | application/pdf or .pdf |
| Office documents | .pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx |
| CSV / spreadsheet import | .csv,.xls,.xlsx,text/csv |
| Any video | video/* |
| Any audio | audio/* |
| Archives | .zip,.tar,.gz,.rar,.7z |
| Any image, camera included | image/* — see capture below to open the camera directly |
| Everything (default) | (omit the prop) |
Opening the camera
capture is its own attribute, not a token inside accept. Pair it with an accept value so the file browser stays useful where there is no camera:
{{-- 1. Restrict to images, as on any upload. --}}
{{-- 2. On a phone, go straight to the rear camera instead of the file browser. --}}
<x-wirekit::file-upload accept="image/*" capture="environment" label="Photo of the damage" />
The two values are environment (rear camera) and user (front). Desktop browsers ignore the attribute and show the ordinary file picker, so it needs no fallback.
PNG, JPG, WebP, or GIF — up to 2 MB
PDF, Word (.doc/.docx), or Excel (.xls/.xlsx) — up to 10 MB
UTF-8 encoded CSV — first row is treated as column headers
Tip: Always pair
acceptwith a human-readablehintthat repeats the allowed formats and size limit. Users often ignore or miss the file picker filter.
Width
Like all WireKit form components, the file upload fills its container (w-full). Control the width via the parent element:
<div class="max-w-md">
<x-wirekit::file-upload label="Resume" name="resume" accept=".pdf" />
</div>
See Input — Width for more layout examples (grid columns, mixed widths).
Compact Variant
The default shape is a drop area: a block-level dashed rectangle that fills its container. That is the right shape for a field in a form, and the wrong one for a dense row — beside a badge and a couple of size="sm" buttons, a full-width area pushes every neighbor onto its own line.
variant="compact" is the same control at button height. It shrinks to its content, lines up with <x-wirekit:: on the same size (they share the height, horizontal-padding and radius tokens), and stays a drop target: dragging files onto the compact control works exactly as it does onto the full area.
The label in a compact control
The shipped default label — Drop files here or click to browse — describes an area, so a compact control that used it would be wider than the row it exists to fit. When you pass no label, the compact variant keeps that sentence as the control's accessible name and does not paint it: the result is an icon-sized drop button that a screen reader still announces correctly.
Pass a short label when you want visible text, as the row above does:
Selected files, the hint and the error message all still render below the control — the compact variant changes the control's shape, not the component's anatomy.
Multiple Files
PNG, JPG, GIF up to 10 MB each
When multiple is true, the name attribute is automatically suffixed with [] so Laravel receives an array. A name you already wrote with [] keeps a single one.
Long Filename Truncation
When a selected file has a name that is longer than the container can hold, the filename is cut in the middle: its start gives way with an ellipsis (…), and its end stays, the extension and the eight characters before it. Two versions of one file usually differ at their end, so quarterly-report-v1.pdf and quarterly-report-v2.pdf still read apart in a narrow column. The file size and the remove button keep their place, and the whole name shows as a tooltip when you point at it.
The preview below sits in a deliberately narrow 20 rem container — drop a file with a long name (e.g. Q4-2026-final-budget-forecast-with-margin-analysis.) into the dropzone to see the ellipsis in action:
The name is two parts in one flexible row: the start may shrink below its width and ends in the ellipsis, the end never shrinks. When the name fits, the two parts stand side by side and read as one; widen the container and the whole name shows.
Error State
File exceeds the 2 MB limit
Errors are shown below the dropzone with aria-invalid="true" + aria-describedby wiring.
Drag & Drop
Files dropped on the zone are automatically assigned to the underlying <input type="file"> via DataTransfer, and a native change event is fired so Livewire's wire:model bindings work unchanged.
Clearing the List
With wire:model, the list follows the bound property. When your action sets the property back to empty (null, an empty string, or an empty array on a multiple upload), Livewire clears the input and the component empties its list with it. There is nothing to wire up:
public function save(): void
{
// 1. Store what was uploaded
foreach ($this->photos as $photo) {
$photo->store('photos');
}
// 2. Empty the property: the input and the list below it clear together
$this->photos = [];
}
It holds the other way round too. When the reader takes the last file out of the list with its remove button, the component sets the bound property to empty, null for one file and an empty array on a multiple upload, so a save afterwards stores nothing the reader removed. Livewire on its own ignores a change that brings no file and would keep the earlier upload.
Without Livewire, a native form reset empties the list as well. For anything else, dispatch wirekit:file-upload-reset with the input's id, from Alpine or from a Livewire action:
<x-wirekit::file-upload label="Attachments" id="attachments" name="attachments" multiple />
<x-wirekit::button x-on:click="$dispatch('wirekit:file-upload-reset', { id: 'attachments' })">
Clear
</x-wirekit::button>
$this->dispatch('wirekit:file-upload-reset', id: 'attachments');
The event names one upload by its id, so other uploads on the page keep their files. A reset is silent: nobody removed a file, so the "Removed …" announcement stays reserved for the remove button.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
required |
bool | false |
Marks the upload as required. Emits the native required attribute on the file input — so the browser blocks submission of an empty upload — plus aria-required="true" and the label indicator (*). |
name |
string|null |
null |
Form field name (appends [] when multiple) |
id |
string|null |
derived | Element id. Without one it is derived from name, then from the bound wire:model property (wire:model="photos" gives wk-upload-photos), and counted in page order when there is neither. A second upload with the same name or property on the page gets -2, so each label and hint names its own input. It is the same on every render, so a Livewire round trip keeps the input instead of replacing it |
multiple |
bool |
false |
Allow multiple file selection |
accept |
string|null |
null |
Accepted MIME types or extensions |
capture |
'user'|'environment'|null |
null |
On a phone, open the camera directly instead of the file browser. environment is the rear camera, user the front one. Ignored on desktop |
size |
string |
'md' |
'sm', 'md', 'lg' |
variant |
string |
'default' |
'default' (full drop area, fills its container) or 'compact' (button-height, shrink-to-fit, for a dense row) |
disabled |
bool |
false |
Disables the field. The dropzone then takes no dropped file and does not light up under one |
label |
string|null |
null → 'Drop files here or click to browse' |
Dropzone label text. In variant="compact" an unset label stays the accessible name but is not painted — see Compact Variant |
removeLabel |
string |
'Remove :name' |
Accessible name template for each file's remove button. The :name placeholder is replaced with the file name at runtime; override it to localize or change the wording |
hint |
string|null |
null |
Helper text below dropzone |
help |
string|null |
null |
An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label |
error |
string|null |
null |
Error message (also reads from $errors) |
scope |
string|null |
null |
Scoped personalization key |
announceError |
bool|null | null |
Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region. |
Accessibility
- Native
<input type="file">preserved (visuallysr-only) — full a11y support - An
autofocuswritten on the tag goes to the file input, the element the keyboard reaches <label>wraps the dropzone so click + keyboard focus work correctly- The focus ring paints on the dropzone itself, not on the input — the input is
sr-only, so a ring drawn on it would sit inside a 1×1 clipped box and never be seen - Error message linked via
aria-describedby+aria-invalid="true" - An
aria-describedbyyou pass reaches the file input, after the component's own hint or error, so a notice elsewhere on the page can describe the upload - Upload icon is
aria-hidden="true"(label text describes the action) variant="compact"without alabelkeeps the default sentence assr-onlytext inside the<label>, so the control is never an unnamed icon —sr-onlyclips the text, it does not remove it from the accessibility tree- The compact control carries
wk-touch-target, which adds a centered 44×44 hit area without changing the painted size (WCAG 2.5.5 AAA) - Each file's remove button takes taps across a centered 44×44 area, and every file row is at least that tall, so the remove buttons of neighboring files never take each other's taps
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus to the file picker control |
Enter / Space |
Open the system file dialog |
Pitfalls
- Don't omit
accept="..."for typed uploads. Withoutaccept, the OS file picker shows everything; users wander into binary files that the server rejects. - Don't bind
wire:model.liveto file inputs. Livewire already streams files via its dedicated upload pipeline —.liveisn't needed and causes redundant requests.
Design Tokens
| Token | Used for |
|---|---|
--text-wk-xs / --text-wk-sm |
Hint / metadata text |
--color-wk-text-muted / --color-wk-text-subtle |
Hint + dropzone instruction text |
--color-wk-bg-muted |
Chip background |
--color-wk-bg-active |
Dropzone background while a file is dragged over it |
--color-wk-bg-hover |
Remove-button hover background |
--color-wk-border-strong |
Dropzone border |
--color-wk-border-error |
Error-state border |
--color-wk-accent |
Active drag-over highlight |
--color-wk-danger-text |
Error message |
--color-wk-ring |
Focus ring |
--ring-wk-width |
Focus ring width |
--radius-wk-sm / --radius-wk-md / --radius-wk-lg |
Dropzone + chip border radius |
--size-wk-sm / --size-wk-md / --size-wk-lg |
Compact-variant control height (same ladder as <x-wirekit::button>) |
--border-wk-width |
Compact-variant border width |
--padding-wk-x-sm / --padding-wk-x-md / --padding-wk-x-lg / --padding-wk-y-xs / --padding-wk-y-sm / --padding-wk-y-md / --padding-wk-y-lg |
Spacing |
--transition-wk-duration |
Drag-over transition |
Config Defaults
The defaults live in config/ under components.. Override them globally:
'components' => [
'file-upload' => ['size' => 'md', 'multiple' => true, 'accept' => 'image/*'],
],