diff --git a/resources/views/components/rich-tooltip.blade.php b/resources/views/components/rich-tooltip.blade.php
new file mode 100644
index 00000000..3e6267e2
--- /dev/null
+++ b/resources/views/components/rich-tooltip.blade.php
@@ -0,0 +1,62 @@
+{{-- An M3 rich tooltip: a few lines of context for a control, with an optional subhead and actions.
+
+
+
+
+
+
+ It wraps its trigger. By default it shows on hover and keyboard focus like a plain tooltip;
+ `persistent` makes it open on press instead and stay until a press elsewhere or Escape — the
+ form M3 asks for when it has actions. The bubble is a popover in surface-container with a
+ medium corner and elevation 2, 312px at most, placed by anchor positioning on `side`.
+
+ RichTooltipTokens (androidx Compose Material 3, Apache-2.0): title-small subhead and body-medium
+ text in on-surface-variant, label-large actions in primary. --}}
+
+@props([
+ 'title' => null,
+ 'text' => null,
+ 'side' => 'bottom',
+ 'persistent' => false,
+])
+
+@php
+ $side = in_array($side, ['top', 'bottom', 'left', 'right'], true) ? $side : 'bottom';
+ $key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
+ $anchor = "--material-rich-tooltip-{$key}";
+@endphp
+
+class('inline-flex') }}
+ style="anchor-name: {{ $anchor }}"
+ x-data="materialRichTooltip({{ $persistent ? 'true' : 'false' }})"
+>
+ {{ $trigger ?? $slot }}
+
+ $side === 'bottom',
+ 'my-1 [position-area:top_span-right] [position-try-fallbacks:flip-block,flip-inline]' => $side === 'top',
+ 'mx-1 [position-area:left_span-bottom] [position-try-fallbacks:flip-inline,flip-block]' => $side === 'left',
+ 'mx-1 [position-area:right_span-bottom] [position-try-fallbacks:flip-inline,flip-block]' => $side === 'right',
+ ])
+ >
+ @if ($title)
+ {{ $title }}
+ @endif
+
+ {{ $text }}
+
+ @isset($actions)
+ {{ $actions }}
+ @endisset
+
+
diff --git a/resources/views/components/stat.blade.php b/resources/views/components/stat.blade.php
new file mode 100644
index 00000000..7808118b
--- /dev/null
+++ b/resources/views/components/stat.blade.php
@@ -0,0 +1,34 @@
+{{-- A figure and what it counts: "1,204 · Shares", "3.2 GB · Stored".
+
+ Not an M3 component; drawn in M3's terms — a panel in surface-container that separates from
+ the page by tone, a label-large `title`, the `value` as the card's hero in an emphasized
+ headline with tabular figures, and a body-small `description`. The value counts up once on
+ first appearance and again when it changes (`x-figure`), and rests under reduced motion.
+ `icon` sits beside the title. The slot, if any, goes under the description — a progress bar
+ for a quota. --}}
+
+@props([
+ 'title' => null,
+ 'value' => null,
+ 'description' => null,
+ 'icon' => null,
+])
+
+
diff --git a/resources/views/components/toast.blade.php b/resources/views/components/toast.blade.php
new file mode 100644
index 00000000..92c1adf8
--- /dev/null
+++ b/resources/views/components/toast.blade.php
@@ -0,0 +1,74 @@
+{{-- The snackbar host: shows every toast, one at a time. Put it once in each layout, near the end
+ of :
+
+
+
+ It shows every `toast` browser event — what `NoNameWeb\LivewireMaterial\Concerns\Toasts`
+ dispatches from a Livewire component — and for `window.materialToast(title, options)` from
+ JavaScript (`{ type, description, timeout, action: { label, handler } }`). Toasts queue and
+ show in turn, each for its `timeout` (4s by default; M3 asks for 4–10s), paused while the
+ pointer or focus is on it. A toast with an action or no timeout gets a close button.
+
+ `@persist` keeps the host across wire:navigate, so a toast dispatched with `redirectTo` is
+ still on screen when the next page arrives.
+
+ M3's snackbar (SnackbarTokens, androidx Compose Material 3, Apache-2.0): inverse surface,
+ body-medium text, a label-large action in inverse-primary, extra-small corners, elevation 3,
+ 48px for one line. A type draws its state icon in the inverse state colour. `position`:
+ `bottom` (centred, the default) or `bottom-start`. It lifts above a bottom bar through
+ `--material-bottom-bar`. --}}
+
+@props(['position' => 'bottom'])
+
+@persist('material-toast')
+
+ <x-badge>, <x-toast> (the snackbar host, fed by the Toasts concern or materialToast()),
+ <x-rich-tooltip>, <x-alert>, <x-stat> and <x-empty-state>.
+
+
+ @foreach ($examples as $title => $code)
+
+ @endforeach
+
diff --git a/tests/Browser/CommunicationTest.php b/tests/Browser/CommunicationTest.php
new file mode 100644
index 00000000..413a82b8
--- /dev/null
+++ b/tests/Browser/CommunicationTest.php
@@ -0,0 +1,46 @@
+waitForEvent('networkidle');
+
+ $page->script("materialToast('First', { type: 'success', timeout: 600 }); materialToast('Second', { type: 'error' })");
+
+ $page->assertScript(SNACKBAR."?.textContent.includes('First')")
+ ->assertScript(SNACKBAR."?.getAttribute('role') === 'status'");
+
+ $page->wait(1.2);
+
+ $page->assertScript(SNACKBAR."?.textContent.includes('Second')")
+ ->assertScript(SNACKBAR."?.getAttribute('role') === 'alert'");
+});
+
+it('shows a toast dispatched as a browser event, as the Toasts concern does', function () {
+ $page = visit('/material')->waitForEvent('networkidle')
+ ->assertScript("typeof window.Livewire !== 'undefined' && typeof window.Alpine !== 'undefined'");
+
+ // Through Livewire, in the page's own realm: an event built inside Playwright's evaluate
+ // carries a detail object Firefox's sandbox will not let the page read.
+ $page->script("window.eval(\"Livewire.dispatch('toast', { type: 'info', title: 'Link copied', description: null, timeout: 4000 })\")");
+
+ $page->assertScript(SNACKBAR."?.textContent.includes('Link copied')");
+});
+
+it('runs a toast\'s action and dismisses it', function () {
+ $page = visit('/material')->waitForEvent('networkidle');
+
+ $page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
+
+ $page->click('[x-data="materialSnackbar"] button:has-text("Undo")')
+ ->assertScript('window.__undone === true')
+ ->assertScript(SNACKBAR.' === null');
+});
+
+it('opens a persistent rich tooltip on press', function () {
+ $bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
+
+ visit('/material')->waitForEvent('networkidle')
+ ->click('#communication button:has-text("Press for details")')
+ ->assertScript("{$bubble}.matches(':popover-open')");
+});
diff --git a/tests/Feature/Components/AlertTest.php b/tests/Feature/Components/AlertTest.php
new file mode 100644
index 00000000..409efecc
--- /dev/null
+++ b/tests/Feature/Components/AlertTest.php
@@ -0,0 +1,34 @@
+blade('');
+
+ expect($html)
+ ->toContain('role="alert"')
+ ->toContain('bg-warning-container text-on-warning-container')
+ ->toContain('Storage almost full')
+ ->toContain('3.8 of 4 GB')
+ ->toContain('
')
+ ->toContain('type-emphasized-headline-md tabular-nums')
+ ->toContain('12 this week')
+ ->toContain('quota')
+ ->toContain('blade('');
+
+ expect($html)
+ ->toContain('x-persist="material-toast"')
+ ->toContain('x-data="materialSnackbar"')
+ ->toContain('bg-inverse-surface')
+ ->toContain('justify-center');
+});
+
+it('can sit at the start', function () {
+ expect((string) $this->blade(''))->toContain('justify-start');
+});