Appearance
Are you an LLM? You can read better optimized documentation at /guide/overlays.md for this page in Markdown format
Overlays & State
Four Blade components cover the interaction vocabulary a storefront section keeps rebuilding: a sheet (modal bottom sheet or side drawer), an empty state, a pill group, and a loading skeleton. Like the carousel, they live in the filamentcraft:: namespace, are styled entirely from your theme's tokens in the package's site.css, use logical CSS properties (RTL needs nothing extra), respect prefers-reduced-motion, and never rely on Tailwind utilities your host build might not ship.
blade
<x-filamentcraft::sheet id="course-plan" :title="$course->title" size="lg">
…
<x-slot:footer>…</x-slot:footer>
</x-filamentcraft::sheet>
<x-filamentcraft::empty-state icon="heroicon-o-magnifying-glass" :title="__('No courses yet')" :hint="__('Try another branch.')" />
<x-filamentcraft::pills name="branch" :options="$branches" wire:model.live="branchId" />
<x-filamentcraft::skeleton lines="3" />Sheet
A modal panel that renders as a bottom sheet with a drag handle on phones, and as a docked bottom sheet (side="bottom") or a full-height drawer on the inline-end edge (side="end") from 640px up.
blade
<button type="button" data-fc-sheet-open="size-guide" aria-haspopup="dialog">
Size guide
</button>
<x-filamentcraft::sheet id="size-guide" title="Size guide" size="md" side="end">
<table>…</table>
<x-slot:footer>
<button type="button" class="fc-btn fc-btn-primary" data-fc-sheet-close>Got it</button>
</x-slot:footer>
</x-filamentcraft::sheet>Props
| Prop | Default | What it does |
|---|---|---|
id | required | DOM id of the <dialog>. Triggers and script events address the sheet by it. |
title | null | Visible heading; the dialog is aria-labelledby it. Without a title, pass your own aria-label. |
size | 'md' | Panel width from 640px up: sm (24rem), md (32rem), lg (44rem). |
side | 'bottom' | bottom docks the panel to the bottom edge; end slides a full-height drawer in from the inline end (the right in LTR, the left in RTL). Phones always get a bottom sheet. |
locale | app locale | Locale for the close button's label. |
close-label | translated | Overrides the close button's aria-label. |
The default slot is the scrollable body; the optional footer slot is pinned below it and accepts attributes (<x-slot:footer class="…">). Any other attribute lands on the <dialog>.
On phones the panel is capped at min(92dvh, 720px) and pads its bottom edge by env(safe-area-inset-bottom), so the footer clears the home indicator.
Opening and closing
The behaviour ships in the public site bundle; there is nothing to register.
- Triggers: any element with
data-fc-sheet-open="{id}"opens that sheet. Triggers getaria-expandedkept in sync. - Closers:
data-fc-sheet-closecloses the sheet it sits in, or the sheet named by its value (data-fc-sheet-close="size-guide") from anywhere on the page. - Escape and a click on the backdrop close it. A text selection dragged out of the panel does not.
- Drag handle: on phones, dragging the handle down more than a quarter of the panel's height dismisses the sheet.
- Focus moves into the sheet on open, stays there while it is open, and returns to the trigger on close. The page behind is inert and does not scroll.
From script or a Livewire component, dispatch a window event with the sheet's id:
php
$this->dispatch('filamentcraft:sheet-open', id: 'course-plan');
$this->dispatch('filamentcraft:sheet-close', id: 'course-plan');js
window.dispatchEvent(new CustomEvent('filamentcraft:sheet-open', { detail: { id: 'course-plan' } }));Both a plain { id } detail and Livewire's array-wrapped payload are accepted. The bundle announces state changes as filamentcraft:sheet-opened and filamentcraft:sheet-closed on window, each with { id } in detail, so an Alpine component can react with x-on:filamentcraft:sheet-closed.window="…".
Why a native <dialog>, not Livewire state
The sheet is a <dialog> opened with showModal(), and its open state lives in the browser, never in a Livewire property:
- The top layer. A modal dialog renders above everything, outside any ancestor's stacking context. A
transform,filterorbackdrop-filteron a sticky header or section wrapper cannot trap it, which is what happens to aposition: fixedoverlay nested inside one. - Accessibility for free. The browser supplies modal semantics, the focus trap,
Escape, and inertness for the rest of the page, the parts hand-rolled overlays most often get wrong. - Morph safety. A Livewire round-trip re-renders the component and would reset a server-side
$openflag, or close the sheet mid-interaction. The dialog carrieswire:ignore.self, so a morph updates its contents without strippingopen, and the script looks the sheet up byidon every event instead of holding a reference a morph could replace. Put the sheet's content under Livewire's control as usual; leave whether it is open to the browser.
Empty state
Plain markup for a listing with nothing in it: an optional icon, a title, a hint, and an optional action. It is static content, so it carries no role="status". If the empty state appears in response to a filter change and should be announced, wrap the results region in your own live region.
blade
<x-filamentcraft::empty-state
icon="heroicon-o-magnifying-glass"
:title="__('No courses match')"
:hint="__('Try another branch or clear the filters.')"
:action-label="__('Clear filters')"
:action-url="$clearUrl" />
<x-filamentcraft::empty-state :title="__('Your cart is empty')">
<x-slot:action>
<button type="button" class="fc-btn fc-btn-primary" wire:click="browse">Browse courses</button>
</x-slot:action>
</x-filamentcraft::empty-state>| Prop | What it does |
|---|---|
icon | Any Blade Icons name (the site already ships Heroicons). Decorative, so aria-hidden. |
title | The main line. |
hint | Supporting copy, capped at a readable measure. |
action-label + action-url | Renders a secondary button link. Ignored when the action slot is given. |
The action slot takes any markup (buttons with wire:click, several links); the default slot adds free content between the hint and the action.
Pills
Single-select chips built from real <input type="radio"> elements in a role="radiogroup", so the browser provides arrow-key roving focus (mirrored in RTL), form submission under name, and wire:model / x-model binding. The chip is the radio's styled label, so there is no aria-pressed to keep in sync.
blade
<x-filamentcraft::pills
name="branch"
:options="$branches->pluck('name', 'id')"
labelledby="branch-heading"
wire:model.live="branchId" />| Prop | What it does |
|---|---|
name | The radios' name, submitted with the form. |
options | value => label array. |
value | The selected value for a plain form (a backed enum works too). Livewire and Alpine bindings set it themselves. |
label / labelledby | Accessible name for the group (aria-label / aria-labelledby). Provide one. |
Every wire:model* and x-model* attribute is forwarded onto each radio input rather than the wrapper, because that is where Livewire and Alpine read and write the value. Other attributes stay on the group.
Skeleton
Placeholder bones for content that is still loading. It is aria-hidden: announce loading state on the real region (for example aria-busy) rather than on the placeholder.
blade
<div wire:loading.remove wire:target="branchId">
@foreach ($courses as $course) … @endforeach
</div>
<x-filamentcraft::skeleton lines="3" avatar wire:loading.block wire:target="branchId" />| Prop | Default | What it does |
|---|---|---|
lines | 3 | Number of text lines (at least one). The last is shorter. |
avatar | false | Adds a round bone beside the lines. |
media | false | Adds a 16:9 media bone above them. |
Use wire:loading.block rather than bare wire:loading, which shows the element as inline-block. The shimmer sweep is switched off under prefers-reduced-motion: reduce.
Styling
Everything is plain classes in site.css, driven by the theme's tokens (--fc-color-surface, --fc-color-on-surface, --fc-color-primary, --box-radius, --button-radius):
- Sheet:
.fc-sheet(with--sm/--md/--lgand--bottom/--end),.fc-sheet__panel,__handle,__header,__title,__close,__body,__footer. The width comes from--fc-sheet-width. - Empty state:
.fc-empty-state,__icon,__title,__hint,__body,__actions. - Pills:
.fc-pills,.fc-pills__option,__input,__label. - Skeleton:
.fc-skeleton,__row,__lines,__bone,__line,__avatar,__media.
Translations
The sheet's close label comes from filamentcraft::filamentcraft.overlays.close. Publish the package lang files to change it. The other components render only the text you pass them.
Related
- Reusable Carousel: the package's other interaction primitive.
- Custom Sections: authoring the section around these components.
- Styling & Tailwind: how package CSS reaches your pages.
