---
url: /guide/outside-panels.md
description: >-
  Use the media picker and library on your own Livewire and Blade pages, with
  standalone mode, assets, policies, plain HTML forms, routes and tenancy.
---

# Outside Panels

The picker, the full library and the display components also work on your own pages: a customer portal, a front-end editor or a plain HTML form. No Filament panel is needed.

## 1. Turn it on

Standalone mode is off by default. Until you enable it, the components refuse to load outside a panel (they return a 403) and the upload routes return 404. In `config/filament-media-library.php`:

```php
'standalone' => [
    'enabled' => true,
    'middleware' => ['web', 'auth'],
],
```

## 2. Check who can manage files

::: warning
Inside a panel, Filament's `canAccessPanel()` decides who gets in. Outside a panel, only your policy does. The bundled `MediaItemPolicy` lets every signed-in user browse and upload shared files. If customers can sign in to your site, register your own policy before you enable standalone mode.
:::

```php
use Hoceineel\FilamentMediaLibrary\Models\MediaItem;
use Hoceineel\FilamentMediaLibrary\Policies\MediaItemPolicy;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Gate;

class AppMediaPolicy extends MediaItemPolicy
{
    public function viewAny(Authenticatable $user): bool
    {
        return $user->is_staff;
    }

    public function create(Authenticatable $user): bool
    {
        return $user->is_staff;
    }
}

// AppServiceProvider::boot()
Gate::policy(MediaItem::class, AppMediaPolicy::class);
```

`viewAny` gates the library and the picker modal. `create` gates uploads. `update`, `delete`, `restore` and `forceDelete` defer to `view`, so override `view` too if you need to restrict those separately. See [Authorization](/reference/authorization) for every ability.

## 3. Load the assets

The page needs Filament's styles and scripts, as in Filament's own guide for using components outside panels. In `resources/css/app.css`:

```css
@import 'tailwindcss';
@import '../../vendor/filament/support/resources/css/index.css';
@import '../../vendor/filament/actions/resources/css/index.css';
@import '../../vendor/filament/forms/resources/css/index.css';
@import '../../vendor/filament/infolists/resources/css/index.css';
@import '../../vendor/filament/notifications/resources/css/index.css';
@import '../../vendor/filament/schemas/resources/css/index.css';

@variant dark (&:where(.dark, .dark *));
```

In your layout:

```blade
<head>
    <style>[x-cloak] { display: none !important; }</style>
    @filamentStyles
    @vite('resources/css/app.css')
</head>
<body>
    {{ $slot }}

    @livewire('notifications')
    @filamentScripts
</body>
```

The library's own CSS and JavaScript load on demand. After an update, run `php artisan filament:assets` again.

## 4. Pick files in a Livewire component

```blade
<livewire:media-library-picker wire:model="coverId" image aspect-ratio="16:9" />
<livewire:media-library-picker wire:model="gallery" multiple image :max-items="12" />
<livewire:media-library-picker wire:model="downloads" multiple accept="application/pdf" folder="Downloads" />
```

```php
use Hoceineel\FilamentMediaLibrary\Models\MediaItem;
use Hoceineel\FilamentMediaLibrary\Rules\MediaLibraryItems;
use Livewire\Component;

class EditPost extends Component
{
    public Post $post;

    public ?int $coverId = null;

    public array $gallery = [];

    public function mount(): void
    {
        $this->coverId = $this->post->cover_id;
        $this->gallery = $this->post->getMediaLibraryItems('gallery')->modelKeys();
    }

    public function save(): void
    {
        $this->validate([
            'coverId' => ['nullable', MediaLibraryItems::image()],
            'gallery' => ['array', 'max:12', MediaLibraryItems::image()],
        ]);

        $this->post->update(['cover_id' => $this->coverId]);
        $this->post->syncMediaLibraryItems($this->gallery, 'gallery');
    }

    public function render()
    {
        return view('livewire.edit-post');
    }
}
```

The picker has the same features as the [form field](/guide/picker-field): drop, paste and upload, chunked uploads, reorder, preview, and the full library in a modal.

| Attribute | Default | Meaning |
|---|---|---|
| `wire:model` or `value` | `null` | Selected id, or a list of ids when `multiple`. An Eloquent collection works too. |
| `name` | `null` | Renders hidden inputs, for a plain `<form>`. |
| `multiple` | `false` | Allow several files. |
| `max-items` | `null` | Limit for `multiple`. |
| `image` | `false` | Shortcut for `accept="image/*"`. |
| `accept` | all types | Comma-separated MIME types. Wildcards work. |
| `folder` | `null` | Folder id or name to open. A name is created when missing and the user may create folders. |
| `lock-folder` | `false` | Keep the picker inside that folder. |
| `layout` | auto | `grid` or `list`. A grid when only images are accepted. |
| `aspect-ratio` | `null` | Frame for a single image, such as `16:9`. |
| `conversion` | `thumb` | Conversion used for previews. |
| `uploadable`, `previewable`, `reorderable`, `show-file-names` | `true` | Switch off with `:uploadable="false"`. |
| `disabled` | `false` | Read-only. |
| `open-on-mount` | `false` | Open the library as soon as the picker renders. |
| `button-label` | "Browse library" | Text of the button that opens the library. |

These options are locked on the server, so a visitor cannot change them from the browser.

::: warning
Always validate submitted ids with `MediaLibraryItems`. It checks that each id exists, that the current user can see it, and that it has an accepted type. Without it, a visitor can submit any id.
:::

## 5. Or in a plain HTML form

```blade
<form method="POST" action="{{ route('posts.update', $post) }}">
    @csrf
    @method('PUT')
    <livewire:media-library-picker name="cover_id" :value="$post->cover_id" image />
    <livewire:media-library-picker name="gallery" :value="$post->getMediaLibraryItems('gallery')" multiple />
    <button>Save</button>
</form>
```

```php
$data = $request->validate([
    'cover_id' => ['nullable', MediaLibraryItems::image()],
    'gallery' => ['nullable', 'array', MediaLibraryItems::image()],
]);
```

With `name`, the picker writes hidden inputs. A single picker posts `cover_id`, and a `multiple` picker posts `gallery[]`. An empty selection posts an empty value, so use `nullable`. After a failed validation, the picker restores the previous selection from `old()`.

## 6. The full library on any page

```blade
<livewire:media-library />
```

It needs the `viewAny` ability, and the user can upload, organise and delete files according to your policy.

## 7. Show files

```blade
<x-media-library-image :item="$post->cover_id" class="aspect-video w-full rounded-2xl object-cover" />
<x-media-library-gallery :record="$post" collection="gallery" :columns="3" aspect-ratio="4:3" />
<x-media-library-gallery :items="$post->gallery_ids" :lightbox="false" :captions="false" />
```

The display components need no sign-in and no standalone switch, because they only read files. The gallery lightbox still needs the Alpine that `@filamentScripts` loads. See [Models & Blade](/guide/models-and-blade) for every attribute.

## Routes and access

| Route | Purpose |
|---|---|
| `POST /media-library/upload` | Chunked uploads. |
| `POST /media-library/upload/resolve` | Answers the duplicate prompt. |
| `GET /media-library/download` | Downloads one or more files. |
| `GET /media-library/serve/{media}/{conversion?}` | Serves files from private disks. Signed links only. |

The first three use `standalone.middleware` (`['web', 'auth']` by default), are throttled to 600 requests a minute, and exist only while `standalone.enabled` is on. The serve route is always available, but only answers valid signed links. Change the `media-library` prefix with `route_prefix`.

Inside a panel, the picker uses the panel's own routes and guard, whatever the standalone setting says.

## What the plugin options do not cover

The plugin's fluent settings (`features()`, `defaultLayout()`, `maxFileSize()`, `acceptedFileTypes()`, `duplicates()`, `canAccess()` and the others) apply only to their panel. Outside panels, set the same options in `config/filament-media-library.php`: the `features`, `browser` and `upload` arrays. Gate access with policies, not `canAccess()`.

## Tenancy

A panel's tenant does not exist outside the panel. To scope front-end pages, turn tenancy on in the config and give it a resolver:

```php
'tenancy' => [
    'enabled' => true,
    'resolver' => App\Support\CurrentTeamResolver::class,
    'strict' => true,
],
```

```php
namespace App\Support;

use Hoceineel\FilamentMediaLibrary\Tenancy\TenantResolver;
use Illuminate\Database\Eloquent\Model;

class CurrentTeamResolver implements TenantResolver
{
    public function resolve(): ?Model
    {
        return auth()->user()?->currentTeam;
    }
}
```

The default resolver reads the Filament panel tenant, which is empty outside a panel. In strict mode that hides every file, so always set your own resolver. Panels with tenancy keep using their own tenant, whatever this config says. See [Tenancy](/guide/tenancy).
