Media
The media package is a first-party media library — an uploadable, searchable file library with a React grid, an inspector for renaming and alt text, folders, bulk actions, and a form field that attaches media to any model through a polymorphic pivot.
Installation
Section titled “Installation”composer require lattice-php/mediaThat is the whole integration: the classes, migrations, config (config/media.php), and the
default Media policy are picked up automatically, and the package ships its React renderer as
source, which the lattice() Vite plugin compiles into your app’s bundle via
virtual:lattice/plugins (see Component packages).
The package has three touchpoints.
The library component — a standalone page of media:
use Lattice\Media\Components\MediaLibrary;
MediaLibrary::make();The picker field — the library inside a form, submitting the selected ids:
use Lattice\Media\Forms\Components\MediaPicker;
MediaPicker::make('gallery')->multiple();MediaPicker::make('gallery', 'Product gallery') ->multiple() ->maxFiles(3)- columnWidth:"md"
- conditions:null
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Product gallery"
- labelAction:null
- maxFiles:3
- multiple:true
- name:"gallery"
- pickerLabel:null
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:false
- selected:null
- tooltip:null
- uploadOnly:false
- value:null
- accept:null
- folders:false
- inspector:true
- picker:true
- signed:false
- actionsLabel:null
- bulkActions:[]
- columns:[]
- data:[{"alt":"A bright workspace with plants","attachments_count":2,"created_at":"2026-08-12T09:15:00+00:00","id":1,"mime_type":"image/jpeg","name":"workspace.jpg","preview_url":"https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=480&q=80","size":248320,"url":"https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1200&q=80"},{"alt":"A modern studio interior","attachments_count":1,"created_at":"2026-08-11T14:40:00+00:00","id":2,"mime_type":"image/jpeg","name":"studio.jpg","preview_url":"https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=480&q=80","size":315904,"url":"https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=1200&q=80"},{"alt":null,"attachments_count":4,"created_at":"2026-08-10T08:05:00+00:00","id":3,"mime_type":"application/pdf","name":"brand-guidelines.pdf","preview_url":null,"size":1843200,"url":null},{"alt":null,"attachments_count":0,"created_at":"2026-08-09T16:20:00+00:00","id":4,"mime_type":"video/mp4","name":"launch-film.mp4","preview_url":null,"size":8294400,"url":null}]
- defaultPerPage:25
- emptyLabel:null
- endpoint:null
- filters:[]
- layout:null
- lazy:false
- pagination:{"currentPage":null,"from":1,"hasMore":false,"lastPage":null,"mode":"none","nextPage":null,"perPage":null,"to":4,"total":4}
- perPageOptions:[]
- pinnableColumns:false
- query:{"filters":[],"mode":null,"page":1,"perPage":25,"search":"","sorts":[],"tableFilterIndicators":[],"tableFilters":{}}
- queryKey:null
- resizableColumns:false
- resizeIndicator:false
- searchable:false
- striped:false
- syncQuery:false
- toolbar:[]
The HasMedia trait — per-collection attachments on any model:
use Lattice\Media\Models\Concerns\HasMedia;
class Product extends Model{ use HasMedia;}
$product->syncMedia($ids, 'gallery'); // replaces the collection, keeps the given order$product->media('gallery'); // MorphToMany<Media>, ordered by the pivot$product->firstMediaUrl('gallery');Validate submitted ids with Lattice\Media\Rules\AttachableMedia.
Browsing: views and the inspector
Section titled “Browsing: views and the inspector”The library renders a toolbar (search, type filter, sort order, grid or list) above the files and
an inspector beside them. Clicking a file makes it the active one; the inspector shows its
preview, metadata, name, alt text, folder, download and delete. Below the lg breakpoint the same
panel arrives as a slideout instead of a column, and selecting several files replaces the details
with a summary of the selection while the bulk bar offers the actions.
MediaLibrary::make()->inspector(false);PDFs and other documents
Section titled “PDFs and other documents”Every accepted file type is listed with a type icon and an extension badge. When lattice-php/pdf is installed, the library composes a viewer template and the inspector renders the selected PDF in it — compact beside the grid, with search and the page sidebar behind Open full view. Nothing to configure: install the package and PDFs preview.
composer require lattice-php/pdfFolders
Section titled “Folders”Folders are user-managed navigation over the same pool of files. They are metadata only — renaming or moving a folder never touches a stored file — and one global tree serves every library.
MediaLibrary::make()->folders();The rail shows All files and Without folder above the folder tree (from lattice-php/tree, so lazy loading, keyboard navigation and drag-and-drop re-parenting come with it). Each folder node carries its own actions — new subfolder, rename, delete — and the file count as a badge. Uploads land in the folder that is open, the inspector files a single file, and Move to folder moves a whole selection.
Folders and categories answer different questions: a category is a developer-owned pool sealed into the component reference, a folder is navigation the user creates and rearranges inside whatever pool they are looking at.
Categories
Section titled “Categories”A category partitions the media pool itself — unlike a collection, which groups attachments per record. Scope a library or picker to one and its listing shows only that category’s media while every upload through it is stamped with the category:
MediaLibrary::make()->category('imports');
MediaPicker::make('file')->category('imports');An unscoped library or picker shows only uncategorized media, so a categorized pool — import files, say — never surfaces next to regular media anywhere it was not explicitly requested.
Upload-only picking
Section titled “Upload-only picking”uploadOnly() skips the library entirely: the button opens the file dialog directly and the
fresh upload becomes the picked value. No browse endpoint is exposed for the field, so existing
media stay invisible. Combined with a category and an upload label this makes a self-contained
“upload an import file” field:
MediaPicker::make('file') ->category('imports') ->uploadOnly() ->uploadLabel(__('imports.upload'));Upload with an inline preview
Section titled “Upload with an inline preview”MediaDropzone is a single-file, upload-only picker whose whole face is the file. Empty, the
face is a click-and-drop target; once a file is stored the face previews it in place — a PDF in
the document viewer when lattice-php/pdf is installed, an image inline, anything
else as its type icon — with the remove control on the preview (in the viewer’s toolbar for PDFs).
Dropping another file onto a filled face replaces it after a confirmation. The field submits the
media id like a single MediaPicker, so the same rules apply:
use Lattice\Media\Forms\Components\MediaDropzone;use Lattice\Media\Rules\AttachableMedia;
MediaDropzone::make('document', __('invoices.document')) ->category('invoices') ->height('70vh') ->emptyText(__('invoices.drop-document')) ->rules(['nullable', 'integer', new AttachableMedia]);height() sizes the face (an int is pixels, a string any CSS length); the preview fills it. On
edit, fill() the stored id and the face opens on that file. category(), uploadLabel(), and
upload rules work as on the picker; multiple(), maxFiles(), and attachmentFields() are
refused — use MediaPicker for those.
Labels
Section titled “Labels”Both buttons a picker renders can be relabeled: pickerLabel() names the trigger that opens the
browse dialog, uploadLabel() (also on MediaLibrary) names the upload button:
MediaPicker::make('file')->pickerLabel('Choose import')->uploadLabel('Upload import file');Rich editor images
Section titled “Rich editor images”The package registers a media-image rich editor extension.
Activate it per field and optionally offer conversions as selectable sizes:
use Lattice\Form\Components\RichEditor;use Lattice\Media\Forms\RichEditor\MediaImage;
RichEditor::make('body')->withExtensions(MediaImage::make()->conversions('hero'));The stored document keeps only {id, alt, conversion} per image — URLs are resolved on every
render and prefill, so temporary/signed disk URLs work. Render stored documents as usual with
RichContent::make($post->body)->toHtml().
To track usage (and benefit from per-collection conversions), sync the referenced media as attachments when you persist the document:
$post->update(['body' => $validated['body']]);$post->syncMedia(MediaImage::idsIn($validated['body']), 'content');Conversion names passed to ->conversions() should be generated for that collection — declare
them in the model’s mediaConversions('content') so the sync dispatches their generation.
Conversions
Section titled “Conversions”Every convertible image (jpeg, png, bmp, gif, webp) gets its derivatives generated by a queued job
after upload and after attach — as does anything stored under a generic mime type, since a signed
upload can record one for a real image; the job probes the bytes and skips what is not an image.
$media->previewConversion() (thumb by default) is what the grid, the picker and the detail
slideout preview; $media->url('thumb') reads any of them and falls back to the original when it
was never generated. Each derivative records its byte size in the conversion map alongside its
path and dimensions; for maps recorded before sizes existed, media:conversions --missing
backfills them from a disk stat without regenerating anything.
Defaults live on the model. Subclass it, point media.model at your class, and return callbacks
over Laravel’s immutable Illuminate\Image\Image — each one must return the transformed image.
Override previewConversion() alongside defaultConversions() if the subclass drops thumb, or
the library grid silently falls back to full-size originals:
class Media extends \Lattice\Media\Models\Media{ public function defaultConversions(): array { return [ 'thumb' => fn (Image $image): Image => $image->cover(400, 400)->optimize('webp', 70), 'hero' => fn (Image $image): Image => $image->scaleDown(1600)->optimize('webp', 80), ]; }}A collection can ask for more on top of the defaults. A bare string reuses a conversion that is already defined globally:
class Product extends Model{ use HasMedia;
public function mediaConversions(string $collection): array { return match ($collection) { 'gallery' => ['card' => fn (Image $image): Image => $image->cover(1200, 800)], 'downloads' => ['hero'], default => [], }; }}Nothing fingerprints a callback, so an edited one is not picked up on its own:
php artisan media:conversions # queue every convertible mediaphp artisan media:conversions --missing # only what is incomplete (or has no dimensions yet)php artisan media:conversions --force # drop the derivatives first, so new specs are adoptedphp artisan media:conversions --force --only=thumb,cardphp artisan media:conversions --id=12 --id=13--only narrows what --force drops and what --missing counts as incomplete; the job itself
always fills in whatever else the media is missing. Dropped derivatives are deleted from the disk,
so a spec that now writes a different file extension leaves nothing behind.
The command regenerates the model’s default conversions; a collection’s extras come from the consuming model, so they are rebuilt the next time that collection is synced. Derivatives are deleted with the media — detaching a media from a record deletes nothing, because another record may rely on the same names.
Upload validation
Section titled “Upload validation”Beyond the accepted types and the size cap, a library can validate every uploaded file:
MediaLibrary::make()->uploadRules(['dimensions:max_width=4000,max_height=4000']);Configuration
Section titled “Configuration”config/media.php covers the disk (media.disk), the upload size cap (media.max_size), the
accepted mime patterns (media.accepted_types, image/* wildcards included, empty accepts
everything), whether uploads go through signed URLs (media.signed_uploads), the media model
(media.model) and the queue the conversion job runs on (media.queue). The previewed
conversion is the model’s previewConversion(), not config — see Conversions.
A single library overrides the config defaults per instance:
MediaLibrary::make()->signedUpload()->disk('s3')->accept('image/*');Translations
Section titled “Translations”The components’ strings ship with inline English defaults. With
laravel-i18next enabled, the plugin’s media
namespace is loaded automatically and serves the bundled en/de translations (override them
like any Laravel package translation — see Internationalization).
Bundle size
Section titled “Bundle size”The breakdown below measures the package’s built plugin — React and the framework runtime are external, so this is exactly what the media package adds on top of an app that already ships Lattice. It is regenerated on every docs build.
95.6 KB gzipped · 361.4 KB raw JavaScript
| Dependency | Raw | Gzip | Share |
|---|---|---|---|
| @lattice-php/media | 41.5 KB | 13.6 KB | 14.2% |
| TipTap + ProseMirror | 290.4 KB | 79.0 KB | 82.7% |
| Bundler runtime | 25.5 KB | 1.5 KB | 1.5% |
| w3c-keyname | 1.3 KB | 0.5 KB | 0.5% |
| orderedmap | 1.7 KB | 0.5 KB | 0.5% |
| use-sync-external-store | 1.0 KB | 0.5 KB | 0.5% |
| Other | 0.0 KB | 0.0 KB | 0.0% |
Emitted JavaScript files
| File | Raw | Gzip |
|---|---|---|
plugin.js | 361.4 KB | 95.6 KB |