Skip to main content
Copy for LLM

File Upload

The <x-wirekit::file-upload> 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

Basic File Upload

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/pdf).

Three ways to specify accepted types

You can mix any of the following inside a single comma-separated string:

  1. File extensions — start with a dot: .pdf, .docx, .csv
  2. MIME types — full type string: application/pdf, image/png
  3. 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.

Images only (PNG, JPG, WebP, GIF)

PNG, JPG, WebP, or GIF — up to 2 MB

Office documents (PDF, Word, Excel)

PDF, Word (.doc/.docx), or Excel (.xls/.xlsx) — up to 10 MB

CSV import

UTF-8 encoded CSV — first row is treated as column headers

Tip: Always pair accept with a human-readable hint that 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::button> 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.

A compact upload inside a dense row
Short by 2

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:

Compact, with and without a visible label

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

Multiple Images

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.pdf) into the dropzone to see the ellipsis in action:

Filename truncation in a narrow container

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 upload with an error message

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 (visually sr-only) — full a11y support
  • An autofocus written 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-describedby you 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 a label keeps the default sentence as sr-only text inside the <label>, so the control is never an unnamed icon — sr-only clips 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. Without accept, the OS file picker shows everything; users wander into binary files that the server rejects.
  • Don't bind wire:model.live to file inputs. Livewire already streams files via its dedicated upload pipeline — .live isn'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/wirekit.php under components.file-upload. Override them globally:

'components' => [
    'file-upload' => ['size' => 'md', 'multiple' => true, 'accept' => 'image/*'],
],

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store