Creating horizontal scrolling containers the right way [CSS Grid]

Dannie Vinther
UX Collective
Published in
6 min readSep 25, 2018

--

Ever since Netflix became a household name we’ve been scrolling sideways in mobile layouts. Instead of stacking everything on top of each other, horizontal scrolling containers (or lists) have become a common layout practice, as it helps reduce the vertical space of devices with smaller screens.

Example of how Netflix handles horizontal scrolling

In this article, I want to explore how the flexibility of CSS Grid can help implement a horizontal scrolling component while dealing with some of the pitfalls that comes with it.

UX considerations

I will not go into a deep discussion about the UX aspects of horizontal scrolling; however, when resorting to a horizontal scrolling layout, it seems that there are at least two UX principles which must be fulfilled:

  1. Your design must have a visual hint that a set of content is horizontally scrollable. The best way to do it is, letting a part of the scrollable content peek out.
  2. It’s important for the user to know when the scroll ends. We have noticed users repeat the scroll operation because they think that they didn’t scroll enough in the previous attempt. A way of indicating the end of the list is use of extra space at the end.
Example of horizontal scrolling with extra space at the end (screenshots from Myntra app)

Outlining the layout

Before we begin, let us outline the layout features we want to accomplish:

  • The scrolling container must follow the overall layout of the page — i.e., respecting the margins and padding
  • Part of the scrollable content must peek out from the edge
  • The content of the container must slide off the edges of the screen when scrolling
  • The gutter between the content must be smaller than that of the edges, so that there will be more space at either end of the container (indicating to the user that they have scrolled to the end)

So something along the lines of this:

Dribbble Shot from Nakul Dhaka — https://dribbble.com/shots/4901594-Horizontal-Vertical-Scroll

Notice that there is an equal amount of space at either end of the horizontal scrolling container matching the surrounding content width.

Overall layout

Now that we have a fundamental understanding of the features we want our horizontal scrolling container to have, let us look into how we might come about coding it using CSS Grid. The convenient thing about CSS Grid is that we can seamlessly control the gutter between the elements without further calculations.

For the overall layout we’ll use a simple yet powerful CSS Grid technique:

.app {
display: grid;
grid-template-columns: 20px 1fr 20px;
}
.app > * {
grid-column: 2 / -2;
}
.app > .full {
grid-column: 1 / -1;
}

Any direct children of .app will be ‘containerized’ with a 20px gap on both ends keeping the content off the edges. If a child is equipped with a class of .full, it will span across the entire viewport without any padding on the side (aka. full bleed).

The scrolling container

Let us create the horizontal scrolling container with six cards, showing two at a time. As we want the horizontal scrolling container to follow the overall layout with padding on both sides, we omit the .full class and might try something like this:

.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
}

Using grid-template-columns we can set up how much space we want each card should take up — in this example, the cards take up 50% of the viewport. When subtracting the gutter, we end up seeing the third card peeking out at the end.

Note that I use CSS variables for the gutter in this Codepen

However — as you might have noticed — the cards are cut off at both ends. Remember, we want the scrollable content to slide of the edges of the screen when we scroll.

So let’s add a class of .full to the container and compensate for the lack of padding:

.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
padding: 0 20px;
}
Note that I use CSS variables for the gutter in this Codepen

At first glance, it seems that we’ve achieved the desired result, but once you scroll to the end, you will notice that there isn’t any space — thus not respecting the overall layout.

You might want to deal with it by adding a margin-right to the last element like so:

.hs > li:last-child {
margin-right: 20px;
}

Unfortunately, this doesn’t work either. So how might we solve it?

Suggested solution

Let’s consider what we have, once we remove the padding of the container:

.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
}

If we add some empty spaces to either side of the grid-template-columns acting as padding, we should be able to achieve our desired layout.

Let’s add 2 x 10px empty spaces to the grid-columns at both ends. Combined with the grid-gap value of 10px, we have 20px in total, thus following the padding of the overall layout.

.hs {
display: grid;
grid-gap: 10px;
grid-template-columns:
10px
repeat(6, calc(50% - 40px))
10px;
grid-template-rows: minmax(150px, 1fr);
}

In order to not having the first card take up the space of the first column of 10px, we bring in empty pseudo elements at each end like so:

.hs::before,
.hs::after {
content: ‘’;
}

The ::before and ::after elements fits perfectly in the grid-columns, as there are automatically added to the start and the end of the horizontal scrolling container. Thankfully, pseudo elements participate in the grid.

Now we are fulfilling all of the layout features we outlined at the beginning:

Caveats

One caveat of this technique is the fixed number of cards you have to specify in the grid-template-columns:

grid-template-columns:
10px
repeat(6, calc(50% - 40px))
10px;

If one of the containers only contains 4 cards, you will need to set up a new grid rule for that particular container. And that’s not very flexible.

One way of making it more flexible is by counting how many cards there are in the specific container using Javascript and then assigning this number to a CSS Variable:

var root = document.documentElement;
const lists = document.querySelectorAll('.hs');
lists.forEach(el => {
const listItems = el.querySelectorAll('li');
const n = el.children.length;
el.style.setProperty('--total', n);
});

Then you can use the variable inside the grid-template-columns:

grid-template-columns:
10px
repeat(var(--total), calc(50% - 40px))
10px;

UPDATE: As Alex Baciu mentions in the comments, you could omit javascript (or a CSS variables solution) entirely by taking advantage of the implicit grid. This way, we don’t need to calculate the number of overflowing columns we need, as this is computed for us by the browser.

For this to work, we will need to set up our code a bit differently:

.hs {
...
grid-template-columns: 10px;
grid-auto-flow: column;
grid-auto-columns: calc(50% - var(--gutter) * 2);
...
...
.hs:before,
.hs:after {
content: '';
width: 10px;
}

We still need our initial 10px to compensate for the padding; however, the rest of the cards are now being laid out by the auto-placement algorithm. In order for this to work though, we need to set auto-flow to ‘column’ (the default is ‘row’).

Finally, we need to make sure, that the .hs:after — which inherit the size of the other cards — doesn’t take up more space than 10 pixels. So we limit the size of the pseudo elements by applying a fixed width.

You could argue, that the code becomes less legible, as the values are scattered a bit more making it somewhat less obvious what is going on. However, I guess that is fine :)

That’s all — my two cents on the matter. I hope you enjoyed it.

Let me know in the comments if you know of another technique or if you just want to comment on this article.

--

--