Five or more sections wrapped onto a 3- or 4-column grid, which left the tab bar's divider under the last row only and stranded every upper row's active indicator against nothing. From five they are now what M3 prescribes and its accessibility page defends: a scrollable tab bar, each tab as wide as its label, offset 52dp. Four or fewer still share the row. The 20px icons take the 20 cut. Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-16). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
177 lines
6.0 KiB
CSS
177 lines
6.0 KiB
CSS
/*
|
|
* M3's tabs (PrimaryNavigationTabTokens, SecondaryNavigationTabTokens, androidx Compose Material 3,
|
|
* Apache-2.0): a 48px tablist on the surface over an outline-variant divider (64px when a primary
|
|
* tab stacks its icon over its label), title-small labels in on-surface-variant, the chosen tab's
|
|
* in primary (primary tabs) or on-surface (secondary tabs).
|
|
*
|
|
* The active indicator is drawn in every tab and shown under the chosen one, so it is right before
|
|
* Alpine starts and after a morph: 3px with rounded top corners under the content of a primary tab
|
|
* (at least 24px wide, inset 2px at each side), 2px across the whole of a secondary one. When the
|
|
* choice changes, a view transition moves it from the old tab to the new (resources/js/tabs.js),
|
|
* as M3's slides.
|
|
*
|
|
* The bar scrolls sideways, so it clips whatever leaves it — including the focus ring, which the
|
|
* rest of the package draws 3px thick 2px outside the element (tokens/state.css). `--tabs-ring`
|
|
* is the room that takes: the bar carries it as padding on every side, and the state layer and the
|
|
* indicator reach back out into it, so the divider still sits against the tabs and the indicator
|
|
* still sits on the divider. That makes the bar 10px taller than M3's 48px tab.
|
|
*/
|
|
|
|
@layer components {
|
|
[data-tabs-bar] {
|
|
--tabs-ring: 5px;
|
|
position: relative;
|
|
display: flex;
|
|
overflow-x: auto;
|
|
overscroll-behavior-x: contain;
|
|
padding: var(--tabs-ring);
|
|
scroll-padding-inline: var(--tabs-ring);
|
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
/* "Offset the first scrollable tab 52dp from the leading edge so it's clear that more content
|
|
is available" (docs/reference/m3/components-navigation-selection-inputs.md § Tabs). Logical,
|
|
so a right-to-left page mirrors it. */
|
|
[data-tabs-bar][data-scrollable] {
|
|
padding-inline-start: 3.25rem;
|
|
scroll-padding-inline-start: 3.25rem;
|
|
}
|
|
|
|
/* A section nav's links sit in list items that share the width. */
|
|
[data-tabs-bar] > li {
|
|
flex: 1 1 0%;
|
|
}
|
|
|
|
[data-tabs-bar] > li > [data-tab] {
|
|
flex: 1 1 0%;
|
|
}
|
|
|
|
[data-tab] {
|
|
position: relative;
|
|
display: flex;
|
|
flex: 1 1 0%;
|
|
min-width: 5.625rem;
|
|
height: 3rem;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding-inline: 1rem;
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
cursor: pointer;
|
|
outline: none;
|
|
font: var(--md-sys-typescale-title-sm);
|
|
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
|
white-space: nowrap;
|
|
transition: color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-tabs-bar][data-scrollable] > li,
|
|
[data-tabs-bar][data-scrollable] [data-tab] {
|
|
flex: none;
|
|
}
|
|
|
|
[data-tabs-bar][data-stacked] [data-tab] {
|
|
height: 4rem;
|
|
}
|
|
|
|
/* The state layer covers the tab and the ring's room around it, so the wash meets the divider. */
|
|
[data-tab]::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset-block: calc(-1 * var(--tabs-ring));
|
|
inset-inline: 0;
|
|
pointer-events: none;
|
|
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-tab]:hover {
|
|
color: var(--md-sys-color-on-surface);
|
|
}
|
|
|
|
[data-tab]:hover::before {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
|
}
|
|
}
|
|
|
|
[data-tab]:focus-visible::before,
|
|
[data-tab]:active::before {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
|
}
|
|
|
|
[data-tab]:focus-visible {
|
|
color: var(--md-sys-color-on-surface);
|
|
outline: 3px solid var(--md-sys-color-secondary);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* A link-based bar marks the page with aria-current, not aria-selected; both are the chosen
|
|
tab, and both take the variant's active colour. */
|
|
[data-tab]:is([aria-selected="true"], [aria-current="page"]) {
|
|
color: var(--md-sys-color-primary);
|
|
}
|
|
|
|
[data-tabs-bar][data-variant="secondary"] [data-tab]:is([aria-selected="true"], [aria-current="page"]) {
|
|
color: var(--md-sys-color-on-surface);
|
|
}
|
|
|
|
[data-tab]:disabled {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
[data-tab]:disabled::before {
|
|
background-color: transparent;
|
|
}
|
|
|
|
[data-tab-content] {
|
|
position: relative;
|
|
display: inline-flex;
|
|
height: 100%;
|
|
min-width: 1.5rem;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
[data-tabs-bar][data-stacked] [data-tab-content] {
|
|
flex-direction: column;
|
|
gap: 0.125rem;
|
|
}
|
|
|
|
/* Primary indicators are inset 2dp at each side; both variants sit on the divider, which the
|
|
ring's room has pushed below the tab. */
|
|
[data-tab-indicator] {
|
|
position: absolute;
|
|
inset-inline: 2px;
|
|
bottom: calc(-1 * var(--tabs-ring));
|
|
height: 3px;
|
|
border-radius: 3px 3px 0 0;
|
|
background-color: var(--md-sys-color-primary);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
view-transition-class: material-tab-indicator;
|
|
}
|
|
|
|
/* A secondary tab's indicator spans the tab, not its content. */
|
|
[data-tabs-bar][data-variant="secondary"] [data-tab-content] {
|
|
position: static;
|
|
}
|
|
|
|
[data-tabs-bar][data-variant="secondary"] [data-tab-indicator] {
|
|
inset-inline: 0;
|
|
height: 2px;
|
|
border-radius: 0;
|
|
}
|
|
|
|
[data-tab]:is([aria-selected="true"], [aria-current="page"]) [data-tab-indicator] {
|
|
opacity: 1;
|
|
view-transition-name: var(--tabs-indicator);
|
|
}
|
|
}
|
|
|
|
::view-transition-group(*.material-tab-indicator) {
|
|
animation-duration: var(--md-sys-motion-spatial-default-duration);
|
|
animation-timing-function: var(--md-sys-motion-spatial-default);
|
|
}
|