Files
Andreas Reinhold / reiniandClaude Opus 5 5800ab1fe1 Leave a toolbar's filled button its own colours
M3's colour list for a standard toolbar ends "Standard button
(Primary)" and the vibrant list ends "Standard button (On primary
container)" (N-13) — both name the *standard* button. The two rules
painted every icon button in the toolbar instead, so a button that
brings its own container got a primary label on a primary fill and its
icon disappeared: SealShare's floating navigation marks the current page
with a filled button, and that button came out a blank circle.

A button that fills its own container is now left alone — filled, tonal,
or either of them selected — and keeps the label colour that goes with
the fill. A disabled button still falls back to button.css's disabled
ink in a toolbar of either colour, as before. AppBarTest pins the
exclusion on both rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:23:16 +02:00

238 lines
13 KiB
CSS

/*
* `<x-toolbar>`: M3 Expressive's toolbars (DockedToolbarTokens.kt, FloatingToolbarTokens.kt,
* androidx Compose Material 3, Apache-2.0; docs/reference/m3/components-navigation-selection-inputs.md
* § Toolbars).
*
* [data-md-toolbar] role="toolbar"; data-md-variant, data-md-vertical, data-md-vibrant,
* data-md-rounded, data-md-toolbar-place
* [data-md-toolbar-fab] a FAB docked at the end, flat on the bar (elevation 0)
* [data-md-toolbar-group] a floating toolbar and its own FAB, side by side (or stacked)
*
* docked a 64px bar across the bottom of the screen in surface-container, square, its
* controls spread out 4 to 32px apart between 16px ends; with a FAB, the controls
* at the start and the FAB flat at the end
* floating a 64px pill in surface-container (or primary-container, `vibrant`) at elevation 3,
* 8px at its ends and 4px between controls; `vertical` stands it on end
*
* Placed over the page, a horizontal toolbar keeps 16dp from the window's edge and a vertical one
* 24dp, which is M3's minimum for each (N-12).
*
* On web and large screens a docked toolbar "can be rounded", and dividers can organise its
* controls into groups: `rounded` does the first from `expanded` (840px), and a divider in the
* slot the second at any width.
*
* `<x-button>` and `<x-fab>` draw their own ink from `data-md-color`/`data-md-variant`, not a
* utility, so a toolbar's recolouring rules — vibrant's icon buttons, a standard toolbar's primary
* ones (N-13), a docked FAB's flattened shadow — sit in this file's own `material.components`
* layer like everything else, each more specific than the button.css or fab.css rule it overrides,
* whichever file an application imports first. The recolouring leaves a disabled button alone, so
* button.css's disabled colours still apply inside a toolbar.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@layer material.components {
[data-md-toolbar] {
display: flex;
align-items: center;
gap: var(--md-sys-measurement-space50);
}
[data-md-toolbar][data-md-variant='docked'] {
width: 100%;
min-height: calc(64px + var(--material-safe-bottom, env(safe-area-inset-bottom)));
justify-content: center;
column-gap: clamp(var(--md-sys-measurement-space50), 4vw, var(--md-sys-measurement-space400));
padding-inline: var(--md-sys-measurement-space200);
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface-variant);
}
[data-md-toolbar][data-md-variant='floating'] {
display: inline-flex;
height: 64px;
padding-inline: var(--md-sys-measurement-space100);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface-variant);
box-shadow: var(--md-sys-elevation-3);
}
[data-md-toolbar][data-md-variant='floating'][data-md-vertical] {
width: 64px;
height: auto;
flex-direction: column;
padding-block: var(--md-sys-measurement-space100);
padding-inline: 0;
}
[data-md-toolbar][data-md-vibrant] {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
[data-md-toolbar][data-md-vibrant] [data-md-icon-button]:not([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-selected='true'], [aria-pressed='true'], :disabled, [aria-disabled='true']) {
color: var(--md-sys-color-on-primary-container);
}
[data-md-toolbar][data-md-vibrant] [data-md-icon-button][aria-pressed='true']:not(:disabled, [aria-disabled='true']) {
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
}
/* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list
ends "Standard button (On primary container)" — the row above (N-13). Both rows name the
*standard* button, so a button that fills its own container is left alone: it carries the
matching label colour (`filled` on-primary, `tonal` on-tone, either selected), and painting
that label primary put a primary icon on a primary fill — the current page's button in a
navigation toolbar came out a blank circle. A disabled button keeps button.css's disabled
colours in either. */
[data-md-toolbar]:not([data-md-vibrant]) [data-md-icon-button]:not([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-selected='true'], [aria-pressed='true'], :disabled, [aria-disabled='true']) {
color: var(--md-sys-color-primary);
}
/* A docked toolbar with a FAB: the controls at the start, the FAB at the end, resting on the
bar at elevation 0 — M3 puts a FAB nested in another component there, and Compose's
FloatingActionButtonDefaults.bottomAppBarFabElevation() is 0 in every state. */
[data-md-toolbar][data-md-variant='docked']:has(> [data-md-toolbar-fab]) {
justify-content: flex-start;
}
[data-md-toolbar-fab] {
display: flex;
margin-inline-start: auto;
}
[data-md-toolbar-fab] [data-md-fab],
[data-md-toolbar-fab] [data-md-fab]:hover {
box-shadow: none;
}
/* Groups of controls, a divider between each two (`<x-divider vertical />`, or `<x-divider />`
in a vertical toolbar). The divider draws itself stretched across its row, which in a
toolbar would run it into the container's edges; here it stands as long as the 40px icon
buttons it separates, centred on them, as the large-screen docked toolbar in M3's
guidelines draws it (toolbars/guidelines § Adaptive design → Resizing). */
[data-md-toolbar]:not([data-md-vertical]) > [role='separator'][aria-orientation='vertical'] {
align-self: center;
height: 40px;
}
[data-md-toolbar][data-md-vertical] > [role='separator'][aria-orientation='horizontal'] {
align-self: center;
width: 40px;
}
/* A floating toolbar and its FAB, side by side (or stacked, vertical). */
[data-md-toolbar-group] {
display: inline-flex;
align-items: center;
gap: var(--md-sys-measurement-space100);
}
[data-md-toolbar-group][data-md-vertical] {
flex-direction: column;
}
/* Placed over the page: centred above the bottom edge, or centred against the end edge.
*
* A toolbar at the bottom shares the screen region with a navigation bar, so it clears the one
* <x-scaffold> publishes as --material-bottom-bar, exactly as the FAB and the snackbar do —
* `max()`, not a sum, because that height already swallows the bottom safe area; without a bar
* the safe area alone applies (N-02). A *docked* toolbar and a navigation bar must never be on
* screen together at all; the offset is the backstop, not a licence.
*/
[data-md-toolbar-place='bottom'] {
position: fixed;
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200));
left: 50%;
z-index: 30;
translate: -50% 0;
}
[data-md-toolbar-place='end'] {
position: fixed;
top: 50%;
inset-inline-end: calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right)));
z-index: 30;
translate: 0 -50%;
}
/* "Horizontal toolbars should have a minimum 16dp margin from the edge of the window. Vertical
toolbars should have a minimum 24dp margin" (N-12). */
[data-md-toolbar][data-md-vertical][data-md-toolbar-place='end'],
[data-md-toolbar-group][data-md-vertical][data-md-toolbar-place='end'] {
inset-inline-end: calc(var(--md-sys-measurement-space300) + var(--material-safe-right, env(safe-area-inset-right)));
}
[data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom'] {
inset-inline: 0;
bottom: var(--material-bottom-bar, 0px);
left: 0;
translate: none;
}
/* The large-screen docked toolbar: from `expanded` (840px) fully rounded, like the one M3 shows
on the web, and spanning what it is written in. At the bottom of the window it lifts off the
edges — 16dp from each side, M3's minimum outside padding for a toolbar, and 16px above the
bottom bar or the safe area, as a floating toolbar sits — so the curve never meets the
window's frame, and the bottom safe area, which it no longer touches, stops padding it.
Below `expanded` it is the square, full-width docked toolbar M3 requires ("Avoid applying
rounded corners to the container"). */
@media (width >= 840px) {
[data-md-toolbar][data-md-variant='docked'][data-md-rounded] {
min-height: 64px;
padding-bottom: 0;
border-radius: var(--md-sys-shape-corner-full);
}
[data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom'] {
right: calc(var(--md-sys-measurement-space200) + var(--material-safe-right, env(safe-area-inset-right)));
bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200));
left: calc(var(--md-sys-measurement-space200) + var(--material-safe-left, env(safe-area-inset-left)));
width: auto;
}
}
/* What a toolbar at the bottom covers, published as --material-bottom-toolbar: the distance from
* the window's bottom edge to the toolbar's top, each formula the placement above plus the
* height. The snackbar clears it (toast.css) — M3 nudges a snackbar "upward to avoid overlapping
* FABs/docked toolbars" and never puts one "in front of … navigation"
* (docs/reference/m3/components-actions-communication-containment.md § Snackbar) — and a page
* pads its end with it, so its last line never hides behind the toolbar.
*
* Declared on the root and again on <x-scaffold>'s root: a custom property resolves its var()s
* where it is declared, and --material-bottom-bar is only set on the scaffold's root, so only
* there does the formula see the bar. A floating toolbar is 64px, beside an 80px (`md`) or
* 96px (`lg`) FAB as tall as the FAB; a docked one is 64px plus the bottom safe area it pads,
* until `rounded` lifts it off the edge from `expanded`. A vertical toolbar, which belongs at
* the end edge, publishes nothing. */
:root:has([data-md-toolbar-place='bottom']:is([data-md-toolbar], [data-md-toolbar-group]):not([data-md-vertical])),
[data-md-scaffold]:has([data-md-toolbar-place='bottom']:is([data-md-toolbar], [data-md-toolbar-group]):not([data-md-vertical])) {
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 64px);
}
:root:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='md']),
[data-md-scaffold]:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='md']) {
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 80px);
}
:root:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='lg']),
[data-md-scaffold]:has([data-md-toolbar-group][data-md-toolbar-place='bottom']:not([data-md-vertical]) [data-md-fab][data-md-size='lg']) {
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 96px);
}
:root:has([data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom']),
[data-md-scaffold]:has([data-md-toolbar][data-md-variant='docked'][data-md-toolbar-place='bottom']) {
--material-bottom-toolbar: calc(var(--material-bottom-bar, 0px) + 64px + var(--material-safe-bottom, env(safe-area-inset-bottom)));
}
@media (width >= 840px) {
:root:has([data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom']),
[data-md-scaffold]:has([data-md-toolbar][data-md-variant='docked'][data-md-rounded][data-md-toolbar-place='bottom']) {
--material-bottom-toolbar: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + var(--md-sys-measurement-space200) + 64px);
}
}
}