M3 Expressive top app bars (small, centered, medium and large flexible, search) that collapse with CSS sticky, docked and floating toolbars, primary and secondary tabs with a view-transition indicator, section navigation, the account menu and theme toggles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
36 KiB
name, description
| name | description |
|---|---|
| livewire-material-development | Build Laravel and Livewire views with Livewire Material's Material 3 Expressive Blade components — props, slots, colour roles, type, shape, motion, theming, toasts, the design guard, and the Livewire traps each component handles. |
Livewire Material Development
When to use this skill
Use this skill when writing or changing any Blade view, Livewire component view or layout in an application that requires nonameweb/livewire-material, and when styling, theming or testing such views.
Setup
Composer packages must be installed before the Vite build (in Dockerfiles and CI alike), because the application's build imports from vendor/:
/* resources/css/app.css */
@import 'tailwindcss';
@import '../../vendor/nonameweb/livewire-material/resources/css/material.css';
@import './material-scheme.css';
@source '../../vendor/nonameweb/livewire-material/resources/views';
@source '../../vendor/nonameweb/livewire-material/src';
// resources/js/app.js
import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
Every layout puts the theme script in <head>, before @vite:
<head>
<x-theme-script />
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
Colour scheme
The scheme is generated, never hand-edited. Regenerate it with the seed and variant recorded at the top of resources/css/material-scheme.css:
php artisan material:scheme "#4f46e5" --variant=tonal-spot
Variants: tonal-spot (M3's default), vibrant, expressive, neutral, fidelity, content, monochrome, rainbow, fruit-salad. --success, --warning and --info set the source of the state colours; --contrast goes from -1 to 1. The command also writes material-scheme.json beside the stylesheet.
Tokens
Tailwind's default palette is cleared: every colour class names an M3 role. text-red-600, bg-base-200 or text-gray-500 compile to nothing.
- Colour roles (
bg-*,text-*,border-*, …):primary,on-primary,primary-container,on-primary-container,inverse-primary,primary-fixed,primary-fixed-dim,on-primary-fixed,on-primary-fixed-variant; the same forsecondaryandtertiary;error,on-error,error-container,on-error-container;success,warningandinfowith theiron-,-containerandon-…-container;inverse-error|success|warning|info;surface,surface-dim,surface-bright,surface-container-lowest|low||high|highest,on-surface,on-surface-variant,inverse-surface,inverse-on-surface,outline,outline-variant,scrim,shadow; pluswhiteandblack. - Ink and lines by meaning:
text-body(body copy),text-meta(metadata),text-quiet(decoration only),border-structure,border-chrome,border-divider/divide-divider. - Type:
type-{display|headline|title|body|label}-{lg|md|sm}andtype-emphasized-…. Never assembletext-*,leading-*andtracking-*by hand. The font is Google Sans Flex (font-sans). - Shape:
rounded-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full}. - Elevation:
shadow-elevation-{1…5}— for what floats over content, not for panels (a panel separates by its container tone). - Motion:
ease-spatial-{fast|default|slow}(position, size, shape; springs that overshoot) andease-effects-{fast|default|slow}(colour, opacity). Always pair an easing with its duration:duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast. Reduced motion zeroes the durations. - States:
state-layer(M3's hover/focus/press overlay; makes the elementrelativeandisolate),focus-ring(keyboard focus indicator),link(a link in running text). dark:follows the page's theme (data-theme), not the operating system.x-figureon an element holding one number counts it up on first appearance and on change.
Theme
config/livewire-material.php → theme.default (light, dark or system), theme.storage_key, theme.legacy_keys. In Alpine, $store.theme holds choice (what the visitor picked), resolved (light or dark, what shows), set('light'|'dark'|'system') and toggle(); x-model="$store.theme.value" binds a control.
Toasts
use NoNameWeb\LivewireMaterial\Concerns\Toasts;
class Settings extends Component
{
use Toasts;
public function save(): void
{
// …
$this->success('Settings saved'); // also warning(), error(), info()
$this->info('Link copied', timeout: 6000);
$this->success('Share created', redirectTo: route('shares.show', $share));
}
}
The methods are protected. They dispatch a toast browser event (assertDispatched('toast', type: 'success', title: 'Settings saved') in tests).
Components
<x-icon>
A Material Symbol (Rounded, weight 400, grade 0, 24px), inline. Every symbol on fonts.google.com/icons exists, by Google's name with underscores. An unknown name throws.
| Prop | Default | |
|---|---|---|
name |
required | calendar_month, cloud_upload, content_copy |
filled |
false |
the filled symbol — M3 uses it for active or selected |
label |
null |
names the icon for screen readers when it carries the meaning alone; otherwise it is aria-hidden |
24px (size-6) unless a size-*, w-* or h-* class is passed. Colour follows the text: <x-icon name="lock" class="size-5 text-on-surface-variant" />.
<x-shape>
One of M3 Expressive's 35 shapes, filled in the text colour, aria-hidden, sized by its caller: <x-shape name="cookie-9" class="size-40 text-secondary-container" />. Names: circle, square, slanted, arch, fan, arrow, semi-circle, oval, pill, triangle, diamond, clam-shell, pentagon, gem, very-sunny, sunny, cookie-4, cookie-6, cookie-7, cookie-9, cookie-12, ghostish, clover-4, clover-8, burst, soft-burst, boom, soft-boom, flower, puffy, puffy-diamond, pixel-circle, pixel-triangle, bun, heart.
<x-theme-script>
The theme decided before the first paint. Exactly once per layout, in <head>, before @vite. No props; configured in config/livewire-material.php.
<x-button>
Label button, icon button, toggle and responsive FAB in one component.
| Prop | Default | |
|---|---|---|
label / slot |
the words; without them and with an icon it is an icon button |
|
variant |
text |
filled, tonal, outlined, elevated, text |
color (alias tone) |
primary |
primary, secondary, tertiary, error, success, warning, info |
primary, danger, caution |
shorthands: filled primary, filled error, filled warning | |
size |
sm |
xs 32px, sm 40px, md 56px, lg 96px, xl 136px |
shape |
round |
or square; both square off further while pressed |
icon, icon-right |
Material Symbol names | |
width |
default |
icon buttons only: narrow, default, wide |
selected |
null |
true/false makes it a toggle (aria-pressed, selected colours and shape) |
link, external, no-wire-navigate |
renders <a>, with wire:navigate unless external |
|
spinner |
true shows the loading indicator while its wire:click runs; a string names the action |
|
tooltip, tooltip-left, tooltip-right, tooltip-bottom |
plain tooltip; also the icon button's accessible name | |
disabled, type, responsive, fab |
responsive hides the label below lg; fab is an extended FAB below sm, a filled button above |
<x-button label="Create link" icon="link" variant="filled" size="md" wire:click="create" spinner />
<x-button icon="delete" tooltip="Delete share" wire:click="delete({{ $share->id }})" />
<x-button icon="favorite" aria-label="Keep" variant="tonal" :selected="$kept" wire:click="toggleKeep" />
<x-tooltip>
M3's plain tooltip, standalone around any trigger: <x-tooltip text="Copy link" side="bottom"><button>…</button></x-tooltip>. side: top (default), bottom, left, right. Shows on hover (fine pointers) and keyboard focus; aria-hidden, so the trigger still needs its own accessible name. Buttons and FABs take a tooltip prop instead.
<x-menu>, <x-menu-item>, <x-menu-group>, <x-menu-separator>
<x-menu label="Share actions" position="bottom-end">
<x-slot:trigger>
<x-button icon="more_vert" tooltip="More" />
</x-slot:trigger>
<x-menu-group label="Sort by">
<x-menu-item label="Newest" :selected="$sort === 'newest'" wire:click="$set('sort', 'newest')" keep-open />
</x-menu-group>
<x-menu-separator />
<x-menu-item label="Settings" icon="settings" link="{{ route('settings') }}" />
<x-menu-item label="Delete" icon="delete" wire:click="delete" description="Recipients lose access" shortcut="⌘⌫" />
</x-menu>
<x-menu>: trigger slot (its first button or link becomes the menu button), label, position (bottom-start default, bottom-end, top-start, top-end), vibrant. <x-menu-item>: label, icon, icon-right, description, shortcut, link, external, selected (makes it a menuitemcheckbox), disabled, keep-open. Choosing an item closes the menu unless keep-open. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab.
<x-button-group>
A row of <x-button>s: <x-button-group label="View" size="md">…</x-button-group>. connected sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the size of the buttons inside.
<x-group>
A choice between a few options as a connected button group of native radios (checkboxes with multiple):
<x-group label="Expires after" wire:model.live="expiry" :options="[
['id' => '1h', 'name' => '1 hour'],
['id' => '1d', 'name' => '1 day', 'icon' => 'today'],
['id' => '7d', 'name' => '7 days', 'disabled' => true],
]" hint="Recipients lose access after that" />
Props: label, hint, name (required with x-model), options, option-value (id), option-label (name), option-icon (icon), size, variant (tonal, filled, outlined), multiple, inline (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint.
<x-split-button>
<x-split-button label="Download all" icon="download" wire:click="downloadZip" menu-label="Download options">
<x-menu-item label="Download files one by one" wire:click="downloadEach" />
</x-split-button>
Attributes go to the leading button; the slot is the menu. variant (filled default, tonal, outlined, elevated), color, size, disabled, spinner, menu-label, position.
<x-fab>
<x-fab icon="add" tooltip="New share" /> — size sm 56px (default), md 80px, lg 96px; with label it is an extended FAB. color primary/secondary/tertiary, drawn in the container, or variant="filled". It does not position itself; wrap it (<div class="fixed end-4 bottom-4">). link, external, disabled, type.
<x-fab-menu>, <x-fab-menu-item>
<div class="fixed end-4 bottom-4">
<x-fab-menu label="New">
<x-fab-menu-item label="Upload files" icon="upload_file" wire:click="uploadFiles" />
<x-fab-menu-item label="Paste text" icon="content_paste" link="{{ route('paste') }}" />
</x-fab-menu>
</div>
Two to six items open above the FAB, which turns into a close button. <x-fab-menu>: icon (add), label, color, position (top-end default). Give items the same color. Keyboard as <x-menu>.
<x-loading>
M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and primary unless sized or coloured by class; contained sets it on a primary-container circle. A progressbar named by label ("Loading"); :label="false" makes it decorative. It rests under reduced motion.
<x-loading />
<x-loading contained class="size-8" label="Uploading" />
<div wire:loading.flex wire:target="upload"><x-loading /></div>
<x-button spinner> shows a decorative one in place of its icon.
<x-toast>
The snackbar host. Once per layout, near the end of <body>: <x-toast /> (position="bottom-start" to leave the centre free). It is @persisted across wire:navigate and shows, one at a time, every toast from the Toasts concern (see Toasts above) or from JavaScript:
materialToast('Share deleted', { type: 'success', description: null, timeout: 4000, action: { label: 'Undo', handler: () => $wire.restore() } })
type (success, error, warning, info) adds the state icon; timeout: 0 keeps it until dismissed; a toast with an action or no timeout gets a close button. Hover or focus pauses the timer.
<x-progress>
M3 Expressive's progress indicator: linear (as wide as its container) or circular (40px, 48px wavy, unless a size-* class is passed), flat or wavy, determinate with a value or indeterminate without one.
| Prop | Default | |
|---|---|---|
value |
null |
0 to max, clamped; null is indeterminate |
max |
100 |
|
bind |
null |
an Alpine expression it follows in the browser; null/undefined is indeterminate |
circular |
false |
circular instead of linear |
wavy |
false |
Expressive's wave (flat below 10% and from 95%) |
thick |
false |
8px track and indicator instead of 4px |
color |
primary |
primary, secondary, tertiary, error, success, warning, info; the track is the colour's container (secondary-container for primary) |
label |
"Progress" |
names the progressbar; :label="false" makes it decorative |
<x-progress :value="$share->uploaded" :max="$share->size" label="Uploading" />
<x-progress circular wavy label="Preparing the download" />
<div x-data="{ progress: null }" x-on:livewire-upload-progress="progress = $event.detail.progress" x-on:livewire-upload-finish="progress = null">
<input type="file" wire:model="file">
<div x-show="progress !== null"><x-progress bind="progress" wavy label="Uploading" /></div>
</div>
A value the server changes animates after a morph (the SVG is wire:ignore; only the root's attributes change). Under reduced motion values jump, the wave stands still and an indeterminate indicator holds one frame. A w-* class narrows a linear one; never pass a display or position class.
<x-badge>
<x-badge />— M3's small badge, a dot.<x-badge value="4" max="99" />— M3's large badge, a count. Botherrorby default.floatingpins it to the top-end corner of arelativeparent:<span class="relative inline-flex"><x-icon name="mail" /><x-badge value="4" floating /></span>. A dot or count isaria-hiddenunless it has alabel; name the control instead ("Messages, 4 unread").<x-badge value="Expired" tonal />,<x-badge value="Active" color="success" tonal />,<x-badge value="Pro" outline />— a status label (not an M3 badge) in the colour's container or a neutral edge.color(aliastone):errordefault,primary,secondary,tertiary,success,warning,info.
<x-alert>
A notice in the page, in the state's container colour with its icon:
<x-alert title="Storage almost full" description="3.8 GB of 4 GB used." color="warning" />
<x-alert color="error" dismissible>
The upload failed.
<x-slot:actions><x-button label="Try again" wire:click="retry" /></x-slot:actions>
</x-alert>
color (alias tone): info (default), success, warning, error, primary, secondary, tertiary, neutral. icon overrides the state icon; :icon="false" removes it. Errors and warnings are role="alert", the rest role="status".
<x-rich-tooltip>
A few lines of context around a trigger, with an optional title and actions slot:
<x-rich-tooltip title="Expiry" text="Recipients lose access after this time.">
<x-button icon="help" aria-label="About expiry" />
</x-rich-tooltip>
Shows on hover and keyboard focus; persistent opens it on press and keeps it until a press elsewhere or Escape (use it when there are actions). side: bottom (default), top, left, right.
<x-stat>
<x-stat title="Shares" value="1,204" icon="link" description="12 this week" /> — a figure on a surface-container panel; the value counts up on first appearance and when it changes. The slot goes under the description (e.g. a quota's <x-progress>). Do not pass a bg-* class; wrap it.
<x-empty-state>
"Nothing here yet": icon on an Expressive shape (cookie-9 by default), title, description or slot, and an actions slot. Use it for an empty collection, not for a filter that matched nothing.
<x-card>
variant: filled (default, surface-container-highest), elevated, outlined; medium corner. Props title, subtitle, separator; slots figure (full-bleed media), menu (top-end), actions (end-aligned). Do not pass bg-*; use variant.
A card or list item that opens something is a row: data-list-row on it and data-list-open on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in <a> or use a stretched link.
<x-card variant="outlined" data-list-row wire:key="share-{{ $share->id }}">
<a href="{{ route('shares.show', $share) }}" data-list-open wire:navigate class="type-title-md">{{ $share->name }}</a>
<x-slot:actions><x-button label="Copy link" wire:click="copy({{ $share->id }})" /></x-slot:actions>
</x-card>
<x-list>, <x-list-item>
<x-list>: label, dividers, segmented (M3 Expressive: separate tiles 2px apart). <x-list-item>: title (or slot), overline, description, leading icon / avatar (image URL or initials) / image / leading slot, trailing trailing text / icon-right / end slot, link (the whole item becomes a row that opens it), selected, disabled. One-, two- and three-line heights follow from the content.
<x-list segmented label="Files">
@foreach ($files as $file)
<x-list-item :title="$file->name" :description="$file->size" icon="description" wire:key="file-{{ $file->id }}">
<x-slot:end><x-button icon="download" tooltip="Download" wire:click="download({{ $file->id }})" /></x-slot:end>
</x-list-item>
@endforeach
</x-list>
<x-divider>
<x-divider /> — outline-variant line; vertical, inset (16px start), middle, decorative (hidden from assistive tech).
<x-collapse>
A disclosure on native <details>: <x-collapse title="Advanced" icon="tune" open variant="filled">…</x-collapse> (variant plain or filled; heading slot for rich titles). Keeps its state through a morph.
<x-modal>
An M3 dialog on native <dialog>. Bind with wire:model to a flag or an id; closing (Escape, scrim, close()) writes back false or null. Without wire:model it uses open from the surrounding Alpine scope.
<x-modal wire:model="deletingId" title="Delete this share?" subtitle="Recipients lose access at once." icon="delete">
<x-slot:actions>
<x-button label="Cancel" x-on:click="close()" />
<x-button label="Delete" danger wire:click="delete" />
</x-slot:actions>
</x-modal>
Props: title, subtitle, icon (centred hero icon), separator, persistent (no Escape or scrim), fullscreen (whole screen below sm, for forms), box-class. Never remove its wire:ignore.self behaviour by re-rendering it conditionally with @if; toggle the bound property instead.
<x-drawer>
An M3 side sheet, bound like <x-modal>; close() in scope. Props: title, subtitle, separator, side (end default, start), width (25rem), with-close-button, close-on-escape (default true), without-backdrop-close, actions slot. pane (with pane-width) turns it into a list-detail pane from xl: render it after the list inside <div class="xl:flex xl:items-start xl:gap-6">. Its body is a size container — lay out inside with @md: etc., not sm:.
<x-bottom-sheet>
An M3 bottom sheet, bound like <x-modal>: modal by default (scrim, inert page, drag the handle down or press Escape to close), standard for one that is part of the page. Props: title, height (90dvh), actions slot.
<x-carousel>, <x-carousel-item>
<x-carousel label="Recent uploads" item-width="220">
@foreach ($photos as $photo)
<x-carousel-item :label="$photo->title" wire:key="photo-{{ $photo->id }}">
<img src="{{ $photo->url }}" alt="{{ $photo->alt }}" />
</x-carousel-item>
@endforeach
</x-carousel>
A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). <x-carousel>: layout (multi-browse default, hero, uncontained, full-screen), item-width (px or any CSS length; the large size multi-browse aims for, the fixed size uncontained keeps, the cap for hero; 186 by default), height (205px), padding (px at the ends, 0), centered (hero), label (the region's name, "Carousel" by default), controls (previous/next buttons: default fine pointers only, true always, false never). <x-carousel-item>: slot is an <img> (fills and crops) or an element sized size-full; label overlays a line of text. A focusable region of slide groups named "n of m"; arrow keys move one item while the row has focus, Home/End to the ends. Works after a Livewire morph, in RTL and under reduced motion. Give items a wire:key in a loop.
<x-chip>
One component for M3's four chips, picked by type:
type |
What it is | Element |
|---|---|---|
assist (default) |
an action | <button>, or <a> with link |
filter |
a toggle | a native checkbox under the chip with wire:model, x-model or name; otherwise a <button aria-pressed> whose selected you own |
input |
something a person entered | its own button only with wire:click, x-on:click, link or selected; a remove button with removable |
suggestion |
a suggested reply or query | <button> |
Props: label / slot, icon, icon-right, elevated (not on input chips), disabled, link, external, no-wire-navigate, selected (filter and input), name / value (a filter checkbox; an input chip's hidden input, value defaulting to the label), avatar (input: image URL or initials), removable, remove (input: an Alpine expression), tooltip. On a filter checkbox and an input chip, class, style and wire:key stay on the chip and every other attribute goes to the control inside.
<x-chip label="Add to calendar" icon="event" wire:click="addToCalendar" />
<x-chip-set label="File types" hint="Show only these" error-field="kinds">
@foreach ($kindOptions as $kind => $name)
<x-chip type="filter" :label="$name" :value="$kind" wire:model.live="kinds" wire:key="kind-{{ $kind }}" />
@endforeach
</x-chip-set>
<x-chip type="filter" label="Starred" icon="star" :selected="$starredOnly" wire:click="$toggle('starredOnly')" />
@foreach ($recipients as $recipient)
<x-chip type="input" :label="$recipient->email" :avatar="$recipient->initials" removable wire:remove="removeRecipient({{ $recipient->id }})" wire:key="recipient-{{ $recipient->id }}" />
@endforeach
- A multi-select set binds
wire:modelon every chip, each with its ownvalue, to an array property; a boolean property needs novalue. The chips render checked as the property already says. - A removable input chip removes through
wire:remove(it becomes the remove button'swire:click),remove(Alpine), or, with neither, takes itself off the page. Backspace or Delete on a focused chip removes it and moves focus to the previous or next chip; the remove button is named "Remove ". Give each one awire:key.
<x-chip-set>
A row of chips 8px apart that wraps, as role="group": label (shown, and names the group; otherwise pass aria-label), hint, error-field (a validation message for that property or its items replaces the hint), scroll (one line that scrolls sideways, fading the edge it can still scroll towards).
<x-form>
A one-column grid of fields with an actions slot at the foot (the slot takes its own class); separator draws a divider above the actions.
<x-form wire:submit="save">
<x-input label="Share name" wire:model="name" required />
<x-select label="Expires after" wire:model="hours" :options="$expiryOptions" />
<x-slot:actions>
<x-button label="Cancel" wire:click="cancel" />
<x-button label="Create share" variant="filled" type="submit" spinner="save" />
</x-slot:actions>
</x-form>
<x-field>, <x-input>, <x-password>, <x-textarea>, <x-select>, <x-file>
M3 text fields. variant: outlined or filled; without it, config('livewire-material.fields.variant') (outlined). All take label, hint, variant, and read their errors from the bag under the wire:model name (the error replaces the hint, sets aria-invalid). class lands on the field's outer element (margins, widths); every other attribute (wire:model, type, required, readonly, autocomplete) reaches the control. Never pass placeholder expecting it to show while a label rests in the field: it shows once the field has focus.
<x-input>:icon,icon-right,prefix,suffix,clearable,copyable(copies the value, confirms with a snackbar),size(sm40px,xs32px — for unlabelled toolbar controls; give themaria-label),mono.<x-password>: a reveal button;icon,size.<x-textarea>: grows fromrows(3) tomax-rows, then scrolls;:autogrow="false"for a fixed, hand-resizable one.<x-select>: native<select>(M3 menu where the browser supports customizable selects).optionsas['id' => …, 'name' => …, 'disabled' => bool],option-value,option-label,placeholder+placeholder-value, or<option>s in the slot;icon,size.<x-file>: native file input; errors fromphotosandphotos.*. Show previews of what was chosen yourself.<x-field id="…" label="…" :messages="$messages">wraps a custom control givenclass="field-control"; only for controls the package does not have.
<x-checkbox>, <x-radio>, <x-toggle>
M3 selection controls on native inputs; the whole row is the label.
<x-checkbox label hint right indeterminate />—indeterminatefor a "select all" whose items are partly ticked (bind it to a server expression; it follows every render).<x-radio label wire:model :options inline />— options['id', 'name', 'hint', 'disabled'](option-value,option-label,option-hint);valuechecks an option withoutwire:model;namenames an unbound group.<x-toggle label hint right icons />— M3 switch (role="switch");iconsputs a check and a cross on the handle,icons="selected"only the check. Withoutlabel, passaria-label.
<x-checkbox label="All files" :checked="count($selected) === $files->count()" :indeterminate="$selected && count($selected) < $files->count()" wire:click="toggleAll" />
<x-toggle label="Notify me on download" wire:model.live="notify" right />
<x-slider>
M3 Expressive's slider on native <input type="range">s (one per handle), so the arrow keys, Home, End, forms and screen readers work as on a plain range; PageUp and PageDown move a tenth of the steps (1 to 10). A press anywhere on the slider moves the nearest handle there. Without JavaScript the native range shows, and posts.
<x-slider label="Volume" wire:model.live="volume" hint="Applies at once" />
<x-slider label="Price" wire:model="price" range :max="500" :step="10" ticks />
<x-slider label="Balance" x-model="balance" name="balance" :min="-50" :max="50" centered />
<x-slider label="Brightness" name="brightness" value="60" size="lg" icon="light_mode" />
| Prop | Default | |
|---|---|---|
label, hint |
the label above names the input (with range, the group); a validation error for the bound property or name replaces the hint |
|
value, min, max, step |
null, 0, 100, 1 |
as on a range input; step="any" is continuous. With wire:model the property's value is drawn |
name |
the wire:model property |
with range it posts name[] twice, from first |
range |
false |
two handles that never cross; binds an array [from, to] (wire:model="price" binds price.0 and price.1, x-model="price" binds price[0] and price[1]) |
centered |
false |
fills from the middle of the track, for values that go below zero |
size |
xs |
track xs 16px, sm 24px, md 40px, lg 56px, xl 96px |
icon |
null |
a Material Symbol inside the track, md and up, standard sliders only |
ticks |
false |
a mark per step (up to 200, hidden while closer than 8px); the handle sits on the marks |
value-label |
drag |
drag (while pressed, dragged or keyboard-focused), always, never |
color |
primary |
primary, secondary, tertiary, error, success, warning, info |
disabled |
false |
Other attributes go to the input(s). A wire:model.live slider sends while it is dragged, and a server render never moves a handle under the pointer (the drawing is wire:ignore); a value the server sets moves the handle after the morph. The binding gets .number, so values arrive as numbers. Its width is the container's unless a w-* class is passed.
<x-choices>
Choosing from a list, with typed values (an array of integers stays integers). options (id, name, disabled; option-value, option-label), label, hint, single. Errors for the property and its items replace the hint.
- Default: filter chips, every option on screen —
singlefor choice chips. searchable: a text field that filters a menu as you type (single value; arrow keys, Enter, Escape);icon,variant,placeholder. Its list is a popover, so it is never clipped by a card.
<x-choices label="Days you are free" wire:model.live="days" :options="$weekdays" />
<x-choices label="Time zone" wire:model="timezone" :options="$timezones" searchable icon="public" />
Bind with wire:model (entangled) or, without Livewire, x-model. The options are baked into its Alpine state: when they change on the server, give it a wire:key that changes with them.
<x-search>
M3 search bar that opens into a search view: docked under the bar from sm, full screen with a back arrow below (docked keeps it docked). Bind the input like any other and render the results in the slot; empty is shown when the slot renders nothing. Choosing a result (a link or button) closes the view; ArrowDown walks the results, Escape closes. Props: placeholder ("Search"), label, icon; trailing slot (avatar, icon buttons).
<x-search wire:model.live.debounce.300ms="query" placeholder="Search shares">
@foreach ($this->results as $share)
<x-list-item :title="$share->name" :description="$share->size" link="{{ route('shares.show', $share) }}" wire:key="result-{{ $share->id }}" />
@endforeach
<x-slot:empty>No shares match.</x-slot:empty>
</x-search>
The docked view overlaps what is under it; never place a search inside an element with overflow-hidden (a card), which clips it.
<x-app-bar>
M3 Expressive top app bar, sticky by default (:sticky="false" to scroll away), turning surface-container once content scrolls under it. variant: small (default), center, medium and large (a big title that collapses into the row as the page scrolls — CSS sticky, no layout shift), search (put an <x-search> in the slot). Props: title, subtitle, heading (h1 default). Slots: navigation (leading icon button), actions (trailing icon buttons, avatar).
<x-app-bar variant="medium" title="Recipients" subtitle="3 people">
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" :link="route('shares.index')" /></x-slot:navigation>
<x-slot:actions><x-button icon="person_add" tooltip="Add recipient" wire:click="add" /></x-slot:actions>
</x-app-bar>
A collapsing bar needs the window to scroll: no ancestor with overflow-hidden/overflow-auto (overflow-x-clip is fine).
<x-toolbar>
M3 Expressive toolbar, role="toolbar" (arrow keys move between controls). variant: floating (default pill at elevation 3; vibrant, vertical) or docked (full-width surface-container bar). place: bottom or end to fix it over the page; fab slot sets a FAB beside a floating toolbar; label names it.
<x-toolbar label="Selection" place="bottom" vibrant>
<x-button icon="download" tooltip="Download" wire:click="download" />
<x-button icon="delete" tooltip="Delete" wire:click="delete" />
<x-slot:fab><x-fab icon="add" tooltip="New share" /></x-slot:fab>
</x-toolbar>
<x-tabs>, <x-tab>
M3 tabs with a server-rendered tablist (arrow keys, Home/End, disabled tabs skipped, the indicator moves in a view transition). tabs: ['name', 'label', 'icon', 'badge', 'disabled']; panels are <x-tab name> in the slot. Bind with wire:model (entangled), or selected / x-model without Livewire. variant primary (default) or secondary; stacked (icon over label), scrollable. Give two identical tab sets on one page distinct ids.
<x-tabs wire:model.live="tab" :tabs="[['name' => 'files', 'label' => 'Files'], ['name' => 'people', 'label' => 'People', 'badge' => $pending]]">
<x-tab name="files">…</x-tab>
<x-tab name="people">…</x-tab>
</x-tabs>
<x-section-nav>
Navigation between the sections of one area (settings, admin): secondary tabs as links from sm (wrapping onto a grid rather than scrolling), a menu picker below. items: ['title', 'url', 'icon', 'active', 'badge'] — current when active or its url is the request's. label, no-wire-navigate.
<x-account-menu>
An avatar that opens a menu: name, email, avatar (image URL or initials; default the name's initials), items in the slot, a theme item (:theme="false" to drop it), and a footer slot for signing out. label, position.
<x-account-menu :name="auth()->user()->name" :email="auth()->user()->email">
<x-menu-item label="Settings" icon="settings" :link="route('settings')" />
<x-slot:footer>
<form method="POST" action="{{ route('logout') }}">@csrf<x-menu-item label="Sign out" icon="logout" type="submit" /></form>
</x-slot:footer>
</x-account-menu>
<x-theme-toggle>
Switches $store.theme: mode="toggle" (default, light/dark icon button), cycle (light → dark → system), picker (segmented buttons for settings pages). Every toggle on a page shares the store.
Testing the design
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
it('uses only what compiles', function () {
expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()])
->forbidColours(['tertiary']) // roles this application's rules leave out
->violations())->toBe([]);
});
It fails on maryUI tags, daisyUI classes, colours the theme does not declare, unknown symbol names and Blade directives written inside a component tag (where they do not compile), with path:line for each.
Conventions
- Components are anonymous Blade components:
<x-name>without a prefix, or<x-{prefix}name>whenconfig('livewire-material.prefix')is set. - Write class names out whole. Tailwind cannot compile
'text-'.$toneortype-{{ $size }}, and the design guard cannot read them. - The showcase at
/material(local only,MATERIAL_SHOWCASE=trueto force it) renders every token and component.
Livewire traps
- Blade directives do not compile inside a component tag's attributes:
<x-foo x-show="ok(@js($value))">reaches the browser as literal text. On a component tag use{{ }}and:propbindings, or put the Alpine on a plain element inside the slot. - Never pass
hidden, a display utility or a position (absolute,relative) to a component: it is merged beside the component's own and whichever Tailwind emits last wins. Wrap the component in an element that carries it. A variant that only hides (max-sm:hidden) is safe. $attributes->wire('model')->value()isfalse, notnull, when there is nowire:model, andfilled(false)is true. Normalise with?: null.- End every statement in a multi-line Alpine attribute with
;: an inline@if … @endifinside it swallows the newline after it.