HTML
<snap-tabs>
<header class="scroll-snap-x">
<nav>
<a active href="#responsive">Responsive</a>
<a href="#accessible">Accessible</a>
<a href="#overscroll">Horizontal Overscroll Ready</a>
<a href="#more"><!-- ...SVG icon --></a>
</nav>
<span class="snap-indicator"></span>
</header>
<section class="scroll-snap-x">
<article id="responsive">
<!-- ...content -->
</article>
<article id="accessible">
<!-- ...content -->
</article>
<article id="overscroll">
<!-- ...content -->
</article>
<article id="more">
<!-- ...content -->
</article>
</section>
</snap-tabs>
CSS
snap-tabs {
--hue: 328deg;
--accent: var(--hue) 100% 54%;
--indicator-size: 2px;
--space-1: .5rem;
--space-2: 1rem;
--space-3: 1.5rem;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
& :matches(header, nav, section, article, a) {
outline-color: hsl(var(--accent));
outline-offset: -5px;
}
}
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
snap-tabs > header {
--text-color: hsl(var(--hue) 5% 40%);
--text-active-color: hsl(var(--hue) 20% 10%);
flex-shrink: 0;
min-block-size: fit-content;
display: flex;
flex-direction: column;
& > nav {
display: flex;
}
& a {
scroll-snap-align: start;
display: inline-flex;
align-items: center;
white-space: nowrap;
font-size: .8rem;
color: var(--text-color);
font-weight: bold;
text-decoration: none;
padding: var(--space-2) var(--space-3);
& > svg {
inline-size: 1.5em;
pointer-events: none;
}
&:hover {
background: hsl(var(--accent) / 5%);
}
&:focus {
outline-offset: -.5ch;
}
}
& > .snap-indicator {
inline-size: 0;
block-size: var(--indicator-size);
border-radius: var(--indicator-size);
background: hsl(var(--accent));
}
}
snap-tabs > section {
block-size: 100%;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
& > article {
scroll-snap-align: start;
overflow-y: auto;
overscroll-behavior-y: contain;
padding: var(--space-2) var(--space-3);
}
}
@media (prefers-reduced-motion: reduce) {
/*
- swap to border-bottom styles
- transition colors
- hide the animated .indicator
*/
snap-tabs {
& > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition:
color .7s ease,
border-color .5s ease;
&:matches(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
& .snap-indicator {
visibility: hidden;
}
}
}
JS
import 'https://argyleink.github.io/scroll-timeline/dist/scroll-timeline.js'
const {matches:motionOK} = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
// grab and stash elements
const tabgroup = document.querySelector('snap-tabs')
const tabsection = tabgroup.querySelector(':scope > section')
const tabnav = tabgroup.querySelector(':scope nav')
const tabnavitems = tabnav.querySelectorAll(':scope a')
const tabindicator = tabgroup.querySelector(':scope .snap-indicator')
/*
shared timeline for .indicator
and nav > a colors */
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection,
orientation: 'inline',
fill: 'both',
})
/*
for each nav link
- animate color based on the scroll timeline
- color is active when it's the current index*/
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
)
})
if (motionOK) {
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
)
}
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active')
tabbtn.setAttribute('active', '')
tabbtn.scrollIntoView()
}
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth
const matchingNavItem = tabnavitems[i]
matchingNavItem && setActiveTab(matchingNavItem)
}
tabnav.addEventListener('click', e => {
if (e.target.nodeName !== "A") return
setActiveTab(e.target)
})
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer)
tabsection.scrollEndTimer = setTimeout(
determineActiveTabSection
, 100)
})
window.onload = () => {
if (location.hash)
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft
determineActiveTabSection()
}
Salvo quando diversamente specificato, i contenuti di questa pagina sono concessi in base alla licenza Creative Commons Attribution 4.0, mentre gli esempi di codice sono concessi in base alla licenza Apache 2.0. Per ulteriori dettagli, consulta le norme del sito di Google Developers. Java è un marchio registrato di Oracle e/o delle sue consociate.
Ultimo aggiornamento 2023-10-25 UTC.
[null,null,["Ultimo aggiornamento 2023-10-25 UTC."],[],[]]