From 98afef7b45fd543d50bdbb969eba2b9db0b08804 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 05:54:52 +0200 Subject: [PATCH] Rest a FAB nested in the rail at elevation 0 M3 puts a FAB nested in another component at level 0, and the documented pattern puts one in the rail's header, so every documented use sat at elevation 3. The rule is unlayered: the FAB's shadow is a utility and a layer would lose to it. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-03). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/navigation.css | 13 +++++++++++++ .../views/components/navigation-rail.blade.php | 3 ++- tests/Feature/Components/NavigationRailTest.php | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/resources/css/components/navigation.css b/resources/css/components/navigation.css index a2f1db1b..6d2d9ba1 100644 --- a/resources/css/components/navigation.css +++ b/resources/css/components/navigation.css @@ -41,6 +41,9 @@ * rail in the layout rather than one that opens over a scrim; 75rem (1200px) is `large`, where the * rail starts expanded instead of collapsed. The bar's own item layout is a *container* query at * the same 37.5rem, so a bar in a narrow column lays out by its own width. + * + * Everything here is in `@layer components` except the last block, which restyles a FAB the + * application put in the rail: that one has to beat a utility, and a layer never does. */ @custom-variant rail-collapsed { @@ -601,3 +604,13 @@ } } } + +/* A FAB in the rail's header rests at elevation 0, not the 3 a standalone FAB has + * (docs/reference/m3/components-navigation-selection-inputs.md § Navigation rail: "when nested + * within another component, such as the navigation rail, the FAB's resting elevation should be + * level 0"). Unlayered on purpose: the FAB draws its shadow with a utility, and a rule in any + * layer loses to a utility — the same reason toolbar.css gives for its vibrant recolouring. */ +[data-navigation-rail-header] [data-fab], +[data-navigation-rail-header] [data-fab]:hover { + box-shadow: none; +} diff --git a/resources/views/components/navigation-rail.blade.php b/resources/views/components/navigation-rail.blade.php index c9b4fa6a..1f712275 100644 --- a/resources/views/components/navigation-rail.blade.php +++ b/resources/views/components/navigation-rail.blade.php @@ -39,7 +39,8 @@ Slots: `brand` beside the menu button, only while expanded; `header` under it — a FAB, drawn as an extended FAB when expanded (`rail-collapsed:` below); the destinations in the default slot, which alone scroll when the window is too short; `footer`, pinned to the foot. Header and - footer never scroll, so nothing in them is cut off by the scroller's edge. + footer never scroll, so nothing in them is cut off by the scroller's edge. A FAB in the header + rests flat: M3 puts a nested FAB at elevation 0, not the 3 a standalone one has. Anything inside can take both shapes with the `rail-collapsed:` variant, which applies while the rail is drawn collapsed for whatever reason: diff --git a/tests/Feature/Components/NavigationRailTest.php b/tests/Feature/Components/NavigationRailTest.php index 99223e02..4f0c54e3 100644 --- a/tests/Feature/Components/NavigationRailTest.php +++ b/tests/Feature/Components/NavigationRailTest.php @@ -51,6 +51,21 @@ it('keeps the header and footer out of the scrolling destinations', function () ->toMatch('/data-navigation-rail-header.*Brand.*FAB.*data-navigation-rail-destinations.*Inbox.*data-navigation-rail-footer.*Account/s'); }); +it('flattens a FAB nested in the rail header to elevation 0', function () { + $html = (string) $this->blade(<<<'BLADE' + + + + BLADE); + + // The hook the unlayered rule in navigation.css needs (N-03). + expect($html)->toContain('data-fab'); + + expect(file_get_contents(__DIR__.'/../../../resources/css/components/navigation.css')) + ->toContain('[data-navigation-rail-header] [data-fab],') + ->toContain('[data-navigation-rail-header] [data-fab]:hover {'); +}); + it('draws a destination in both shapes, with its count on the icon and at the end', function () { $html = (string) $this->blade('');