รูปแบบนี้จะแสดงวิธีสร้างคอมโพเนนต์แท็บที่มีตารางกริดและการเลื่อนสแนปพอยท์
บทความเต็ม · วิดีโอบน YouTube · แหล่งที่มาเกี่ยวกับ GitHub
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() }