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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:54:52 +02:00
co-authored by Claude Opus 5
parent 4d4862189a
commit 98afef7b45
3 changed files with 30 additions and 1 deletions
+13
View File
@@ -41,6 +41,9 @@
* rail in the layout rather than one that opens over a scrim; 75rem (1200px) is `large`, where the * 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 * 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. * 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 { @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;
}
@@ -39,7 +39,8 @@
Slots: `brand` beside the menu button, only while expanded; `header` under it — a FAB, drawn 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 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 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 Anything inside can take both shapes with the `rail-collapsed:` variant, which applies while
the rail is drawn collapsed for whatever reason: the rail is drawn collapsed for whatever reason:
@@ -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'); ->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'
<x-navigation-rail mode="expanded">
<x-slot:header><x-fab icon="edit" tooltip-right="Compose" /></x-slot:header>
</x-navigation-rail>
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 () { it('draws a destination in both shapes, with its count on the icon and at the end', function () {
$html = (string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />'); $html = (string) $this->blade('<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />');