Colour, shape, type, elevation and motion as tokens and Tailwind utilities; `php artisan material:scheme`, which generates an app's colour roles with Google's material-color-utilities (spec 2025); the theme head script with light, dark and system and its Alpine store; Google Sans Flex; every Material Symbol (4,135, outlined and filled) drawn by <x-icon> without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as <x-shape>; the x-figure directive; the Toasts concern; DesignGuard for applications' tests; and a showcase with every token, both themes side by side and an icon search. Colour utilities are `@theme inline`, so a section with its own data-theme repaints; without it they resolve once on :root. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
7.8 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.
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 and unknown symbol names, 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.