แท็บ

รูปแบบนี้จะแสดงวิธีสร้างคอมโพเนนต์แท็บที่มีตารางกริดและการเลื่อนสแนปพอยท์

บทความเต็ม · วิดีโอบน 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() }