Câu chuyện

Mẫu này trình bày cách xây dựng một thành phần Stories cho web có tính thích ứng, hỗ trợ di chuyển bằng bàn phím và hoạt động trên nhiều trình duyệt.

Bài viết đầy đủ · Video trên YouTube · Nguồn trên GitHub

<div class="stories"> 
 
<section class="user">
   
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
   
<article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
 
</section>
 
<section class="user">
   
<article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
 
</section>
 
<section class="user">
   
<article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
 
</section>
 
<section class="user">
   
<article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
   
<article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
   
<article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
 
</section>
</div>

       
.stories {
 
display: grid;
 
grid: 1fr / auto-flow 100%;
 
grid-gap: 1ch;
 
gap: 1ch;
 
overflow-x: auto;
 
scroll-snap-type: x mandatory;
 
overscroll-behavior: contain;
 
touch-action: pan-x;
}

.user {
 
scroll-snap-align: start;
 
scroll-snap-stop: always;
 
display: grid;
 
grid: [story] 1fr / [story] 1fr;
}

.story {
 
grid-area: story;

 
background-size: cover;
 
background-image:
    var
(--bg),
    linear-gradient
(to top, rgb(249, 249, 249), rgb(226, 226, 226));

 
user-select: none;
 
touch-action: manipulation;

 
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1)
}

.story.seen {
 
opacity: 0;
 
pointer-events: none;
}
       

       
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story
: stories.firstElementChild.lastElementChild
}

const navigateStories = direction => {
 
const story = state.current_story
 
const lastItemInUserStory = story.parentNode.firstElementChild
 
const firstItemInUserStory = story.parentNode.lastElementChild
 
const hasNextUserStory = story.parentElement.nextElementSibling
 
const hasPrevUserStory = story.parentElement.previousElementSibling
 
 
if (direction === 'next') {
   
if (lastItemInUserStory === story && !hasNextUserStory)
     
return
   
else if (lastItemInUserStory === story && hasNextUserStory) {
      state
.current_story = story.parentElement.nextElementSibling.lastElementChild
      story
.parentElement.nextElementSibling.scrollIntoView({
        behavior
: 'smooth'
     
})
   
}
   
else {
      story
.classList.add('seen')
      state
.current_story = story.previousElementSibling
   
}
 
}
 
else if(direction === 'prev') {
   
if (firstItemInUserStory === story && !hasPrevUserStory)
     
return
   
else if (firstItemInUserStory === story && hasPrevUserStory) {
      state
.current_story = story.parentElement.previousElementSibling.firstElementChild
      story
.parentElement.previousElementSibling.scrollIntoView({
        behavior
: 'smooth'
     
})
   
}
   
else {
      story
.nextElementSibling.classList.remove('seen')
      state
.current_story = story.nextElementSibling
   
}
 
}
}

stories
.addEventListener('click', e => {
 
if (e.target.nodeName !== 'ARTICLE')
   
return
 
  navigateStories
(
    e
.clientX > median
     
? 'next'
     
: 'prev')
})

// left & right are free with snap points 👍
document
.addEventListener('keydown', ({key}) => {
 
if (key !== 'ArrowDown' || key !== 'ArrowUp')
    navigateStories
(
      key
=== 'ArrowDown'
       
? 'next'
       
: 'prev')
})