/* * 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); }