Media
Media captures exactly one image or one document into Laravel Storage and
binds a MediaValue on your native component. Use it when you need a form
field with preview, clear, optional crop, and the same error slot as other
Firstlight fields.
Complete example
Use FirstlightUI\NativeComponent for the full nonstreaming
response-revision contract.
<?php
namespace App\NativeComponents;
use FirstlightUI\Media\MediaStorage;
use FirstlightUI\Media\MediaValue;
use Illuminate\View\View;
use FirstlightUI\NativeComponent;
class Profile extends NativeComponent
{
public ?MediaValue $avatar = null;
/** @var array<string, string> */
protected array $rules = [
'avatar' => 'required|image|max:2048',
];
public function avatarChosen(string $tempPath): void
{
$previous = $this->avatar;
$this->avatar = MediaStorage::commit(
$tempPath,
'mobile_public',
'avatars',
);
MediaStorage::delete($previous);
}
public function avatarCleared(): void
{
MediaStorage::delete($this->avatar);
$this->avatar = null;
}
public function render(): View
{
return view('native.profile');
}
}
<firstlight:media
mode="image"
label="Profile photo"
helper="Square crop required"
aspect="1:1"
disk="mobile_public"
directory="avatars"
native:model="avatar"
@change="avatarChosen"
@clear="avatarCleared"
/>
Props
| API | Accepted type | Purpose |
|---|---|---|
mode |
image | document |
Required. Image allows camera/library and crop; document uses the system file picker only. |
value |
MediaValue | null |
Committed value. Empty is null. |
native:model |
a ?MediaValue component property |
Synchronises the accepted value. native:model.live is also accepted. |
label |
string |
Visible field label. |
helper |
string |
Supporting text below the control. |
error |
string |
Validation text; shown instead of helper when non-empty. |
required |
bool |
Marks the field as required visually and for assistive technology. |
disabled |
bool |
Prevents pick and clear while retaining the accepted value. |
disk |
string |
Laravel Storage disk. Defaults to mobile_public. |
directory |
string |
Directory prefix under the disk. Defaults to media. |
aspect |
ratio string such as 1:1 |
Image only. Implies required crop to that aspect. |
crop |
optional | required |
Image only. Freeform crop when aspect is omitted. |
a11y-label |
string |
Explicit accessible name when needed. |
a11y-hint |
string |
Additional VoiceOver and TalkBack guidance. |
class |
string |
External EDGE layout utilities. |
Document mode rejects crop and aspect. Unsupported attributes such as
multiple and video are rejected before publication.
Crop rules
- Neither
cropnoraspect→ store without a crop sheet. aspect→ required crop locked to that aspect.crop="optional"without aspect → freeform crop; Skip is allowed.crop="required"without aspect → freeform crop; Confirm is required.
Confirm stores the region the crop sheet shows: the largest rectangle at the locked aspect (or the photo's own aspect when freeform), narrowed by zoom and placed where the person moved it. The region starts centred. Drag the photo to move it and pinch or use the zoom buttons to zoom from 1× to 4×. The region never leaves the photo: it stops at the edges, and zooming out pulls it back inside. With a locked aspect that differs from the photo's, the region can move along the longer side even at 1×; freeform at 1× is the whole photo. A new pick starts centred again. Skip stores the whole photo. Image mode always stores a JPEG.
Clear, Skip, Confirm, Cancel, Crop, zoom, crop-move, and source-chooser copy are package chrome; see Localize chrome.
Events
@change receives the temporary absolute file path after the user confirms a
pick (and crop when required). Commit with MediaStorage::commit before
assigning MediaValue.
@clear fires when the user clears a present value. Delete the previous
object with MediaStorage::delete and set the model to null.
MediaValue
new MediaValue(
disk: 'mobile_public',
path: 'avatars/a.jpg',
mime: 'image/jpeg',
size: 1200,
width: 100,
height: 100,
);
In image mode the field shows a thumbnail of the stored file. It is decoded
from the file's local path, so it appears only for disks with a local driver,
such as the default mobile_public; other disks show a photo icon. Document
mode always shows a document icon and the file name.
To show the image elsewhere in a native view, use the file's local path,
Storage::disk($value->disk)->path($value->path). The disk's url() is
served only to WebViews.
Validation
Media participates in Validate fields. Public
MediaValue properties are adapted to Laravel UploadedFile values so stock
image, file, mimes, max, and dimensions rules work. null fails
required. Authored error wins over the MessageBag binder.
Accessibility
The field exposes a labelled control with helper or error text. Crop sheets include Confirm, Cancel, optional Skip, and explicit zoom in/out controls meeting 44-point (iOS) and 48-dp (Android) minima. Zoom is not pinch-only, and moving the region is not drag-only: the crop region is one labelled element with Move crop left, right, up, and down actions (VoiceOver actions rotor, TalkBack actions menu). Each action moves a tenth of the region.
Platform behaviour
iOS offers Photo Library (PhotosPicker) and Camera in a confirmation dialog,
uses fileImporter for documents, and shows a Firstlight SwiftUI crop sheet.
Android offers Photo Library (the system photo picker) and Camera (the system
camera app) in a Material bottom sheet, uses the system document picker, and
shows a Firstlight Compose crop dialog. Picked files are copied to the app's
temporary directory before @change receives the path. Geometry and system
pickers stay platform-native; crop semantics stay shared.
Host application setup
On iOS, image mode's Camera option requires a camera usage description, or
iOS terminates the app when the camera opens. Firstlight's plugin manifest
supplies a generic one ("Used to take a photo for a form field."). Override it
in config/nativephp.php with wording specific to your app for App Store
review; app entries win over plugin entries:
'permissions' => [
'NSCameraUsageDescription' => 'Used to take a photo for your profile.',
],
The photo picker and document picker need no permission on either platform.
Android needs no manifest change: the system camera app writes through the
FileProvider that NativePHP's Android host already declares. If your app adds
the CAMERA permission for another feature, grant it before offering Camera.
Compatibility
Media supports the versions listed in the current compatibility reference. The host application must compile both Firstlight-owned native renderers declared by the package manifest.
Screenshots
| Platform | Light | Dark |
|---|---|---|
| iOS | ![]() |
![]() |
| Android | ![]() |
![]() |



