Building a Stories component

In this post I want to share thinking on building a Stories component for
the web that is responsive, supports keyboard navigation, and works across
browsers.

Demo

If you would prefer a hands-on demonstration of building thi…

In this post I want to share thinking on building a Stories component for
the web that is responsive, supports keyboard navigation, and works across
browsers.

Demo

If you would prefer a hands-on demonstration of building this Stories component yourself,
check out the Stories component codelab.

If you prefer video, here’s a YouTube version of this post:

Overview

Two popular examples of the Stories UX are Snapchat Stories and Instagram Stories (not to mention fleets).
In general UX terms, Stories are usually a mobile-only, tap-centric pattern for navigating
multiple subscriptions. For example, on Instagram, users open a friend’s story
and go through the pictures in it. They generally do this many friends at a
time. By tapping on the right side of the device, a user skips ahead to that friend’s
next story. By swiping right, a user skips ahead to a different friend.
A Story component is fairly similar to a carousel, but allows navigating a
multi-dimensional array as opposed to a single-dimensional array. It’s as if there’s a carousel inside
each carousel. ?

Visualized multi-dimensional array using cards. Left to right is a stack of purple borders cards, and inside each card is 1-many cyan bordered cards. List in a list.
1st carousel of friends

2nd “stacked” carousel of stories

? List in a list, aka: a multi-dimensional array

Picking the right tools for the job

All in all I found this component pretty straightforward to build, thanks to a few
critical web platform features. Let’s cover them!

CSS Grid

Our layout turned out to be no tall order for CSS Grid as it’s equipped with some
powerful ways to wrangle content.

Friends layout

Our primary .stories component wrapper is a mobile-first horizontal scrollview:

.stories {
inline-size: 100vw;
block-size: 100vh;

display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;

overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior: contain;
touch-action: pan-x;
}

/* desktop constraint */
@media (hover: hover) and (min-width: 480px) {
max-inline-size: 480px;
max-block-size: 848px;
}
Using Chrome DevTools’
Device Mode
to highlight the columns created by Grid

Let’s breakdown that grid layout:

  • We explicitly fill the viewport on mobile with 100vh and 100vw and constrain the size on desktop
  • / separates our row and column templates
  • auto-flow translates to grid-auto-flow: column
  • The autoflow template is 100%, which in this case is whatever the scroll window width is

Note that the location of the / separator relative to auto-flow is important.
If auto-flow came before / it would be shorthand for grid-auto-flow: row.

On a mobile phone, think of this like the row size being the viewport height and
each column being the viewport width. Continuing with the Snapchat Stories and
Instagram Stories example, each column will be a friend’s story. We want friends
stories to continue outside of the viewport so we have somewhere to scroll to.
Grid will make however many columns it needs to layout your HTML for each friend
story, creating a dynamic and responsive scrolling container for us. Grid
enabled us to centralize the whole effect.

Stacking

For each friend we need their stories in a pagination-ready state.
In preparation for animation and other fun patterns, I chose a stack.
When I say stack, I mean like you’re looking down on a
sandwich, not like you’re looking from the side.

With CSS grid, we can define a single-cell grid (i.e. a square), where the rows
and columns share an alias ([story]), and then each child gets assigned to that
aliased single-cell space:

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

}

This puts our HTML in control of the stacking order and also keeps all elements
in flow. Notice how we didn’t need to do anything with absolute positioning or z-index and
we didn’t need to box correct with height: 100% or width: 100%. The parent grid
already defined the size of the story picture viewport, so none of these story components
needed to be told to fill it!

CSS Scroll Snap Points

The CSS Scroll Snap Points spec makes it
a cinch to lock elements into the viewport on scroll. Before these CSS properties existed,
you had to use JavaScript, and it was… tricky, to say the least. Check out
Introducing CSS Scroll Snap Points
by Sarah Drasner for a great breakdown of how to use them.

Horizontal scrolling without and with scroll-snap-points styles.
Without it, users can free scroll as normal. With it, the browser rests gently on each item.

parent

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

Parent with overscroll defines snap behavior.

child

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

Children opt into being a snap target.

I chose Scroll Snap Points for a few reasons:

  • Free accessibility. The Scroll Snap Points spec states that pressing the
    Left Arrow and Right Arrow keys should move through the snap points
    by default.
  • A growing spec. The Scroll Snap Points spec is getting new features and improvements
    all the time, which means that my Stories component will probably only get better from
    here on out.
  • Ease of implementation. Scroll Snap Points are practically built for the
    touch-centric horizontal-pagination use case.
  • Free native inertia. Every platform will scroll and rest in its style, as opposed to
    normalized inertia which can have an uncanny scrolling and resting style.

Cross-browser compatibility

We tested on Opera, Firefox, Safari, and Chrome, plus Android and iOS. Here’s
a brief rundown of the web features where we found differences in capabilities and support.

Success:
All of the features chosen were supported and none were buggy.

We did though have some CSS not apply, so some platforms are currently missing out on UX
optimizations. I did enjoy not needing to manage these features and feel confident
that they’ll eventually reach other browsers and platforms.

scroll-snap-stop

Carousels were one of the major UX use cases that prompted the creation of the
CSS Scroll Snap Points spec. Unlike Stories, a carousel doesn’t always need to stop
on each image after a user interacts with it. It might be fine or encouraged to
quickly cycle through the carousel. Stories, on the other hand, are best navigated one-by-one,
and that’s exactly what scroll-snap-stop provides.

.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}

At the time of writing this post, scroll-snap-stop is only supported on Chromium-based
browsers. Check out
Browser compatibility
for updates. It’s not a blocker, though. It just means that on unsupported browsers
users can accidentally skip a friend. So users will just have to be more careful, or
we’ll need to write JavaScript to ensure that a skipped friend isn’t marked as viewed.

Read more in the
spec
if you’re
interested.

overscroll-behavior

Have you ever been scrolling through a modal when all of a sudden you
start scrolling the content behind the modal?
overscroll-behavior
lets the developer trap that scroll and never let it
leave. It’s nice for all sorts of occasions. My Stories component uses it
to prevent additional swipes and scrolling gestures from leaving the
component.

.stories {
overflow-x: auto;
overscroll-behavior: contain;
}

Safari and Opera were the 2 browsers that didn’t
support this, and that’s
totally OK. Those users will get an overscroll experience like they’re used to
and may never notice this enhancement. I’m personally a big fan and like
including it as part of nearly every overscroll feature I implement. It’s a
harmless addition that can only lead to improved UX.

scrollIntoView({behavior: 'smooth'})

When a user taps or clicks and has reached the end of a friend’s set of stories,
it’s time to move to the next friend in the scroll snap point set. With
JavaScript, we were able to reference the next friend and request for it to be
scrolled into view. The support for the basics of this are great; every browser
scrolled it into view. But, not every browser did it 'smooth'. This just means
it’s scrolled into view instead of snapped.

element.scrollIntoView({
behavior: 'smooth'
})

Safari was the only browser not to support behavior: 'smooth' here. Check out
Browser compatibility
for updates.

Hands-on

Now that you know how I did it, how would you?! Let’s diversify our
approaches and learn all the ways to build on the web. Create a Glitch,
tweet me your version, and I’ll add it to
the Community remixes section below.

Community remixes


Print Share Comment Cite Upload Translate
APA
Adam Argyle | Sciencx (2024-03-29T09:06:00+00:00) » Building a Stories component. Retrieved from https://www.scien.cx/2020/11/25/building-a-stories-component/.
MLA
" » Building a Stories component." Adam Argyle | Sciencx - Wednesday November 25, 2020, https://www.scien.cx/2020/11/25/building-a-stories-component/
HARVARD
Adam Argyle | Sciencx Wednesday November 25, 2020 » Building a Stories component., viewed 2024-03-29T09:06:00+00:00,<https://www.scien.cx/2020/11/25/building-a-stories-component/>
VANCOUVER
Adam Argyle | Sciencx - » Building a Stories component. [Internet]. [Accessed 2024-03-29T09:06:00+00:00]. Available from: https://www.scien.cx/2020/11/25/building-a-stories-component/
CHICAGO
" » Building a Stories component." Adam Argyle | Sciencx - Accessed 2024-03-29T09:06:00+00:00. https://www.scien.cx/2020/11/25/building-a-stories-component/
IEEE
" » Building a Stories component." Adam Argyle | Sciencx [Online]. Available: https://www.scien.cx/2020/11/25/building-a-stories-component/. [Accessed: 2024-03-29T09:06:00+00:00]
rf:citation
» Building a Stories component | Adam Argyle | Sciencx | https://www.scien.cx/2020/11/25/building-a-stories-component/ | 2024-03-29T09:06:00+00:00
https://github.com/addpipe/simple-recorderjs-demo