Horizontally Scrolling Panes with clean HTML, modern CSS and no JS

This used to be a really hard problem, new CSS tools make it simpler. Here’s how it works…

Ada Rose Cannon
Mar 11, 2020 · 4 min read

Update (09/23/2020): an experimental feature called scroll-snap-stop makes the user experience of this even better, the article has been updated accordingly.

Isn’t it nice when the tools you work with get better and better when you’re not looking? That’s what it feels like working in the Web some days.

Many years ago when I was still a front-end engineer on real products, I helped maintain a very popular newspaper site. It was one of the very early Web Apps designed to be responsive and touch first.

Since it was a newspaper the aesthetic had pages sat next to each other which could be swiped between with a flick of a finger.

3 Pages laid out horizontally

It was a huge hack to implement this. It involves significant amounts of JavaScript and some really awkward HTML. It was flaky and sometimes suffered from poor performance, since we had to constantly track user input.

I tried remaking it for a project today and I was able to build the whole thing with only CSS!

Here’s how… (Demo at the bottom, direct link)

The markup.

<h1>The Pink Paper</h1><h2>It's Salmon Actually</h2>
<li><a href="#article1">Article 1</a></li>
<li><a href="#article2">Article 2</a></li>
<li><a href="#article3">Article 3</a></li>
<article id="article1">Some content</article>
<article id="article2">Some content</article>
<article id="article3">Some content</article>

We have 3 <article> which will contain our content.

We have a <nav> with 3 links, deep linking to the content we don’t want to break this behavior since it is useful for people who can’t scroll in two dimensions such as people using a mouse.

Layout the <article> elements horizontally.

We will use CSS Grid to do that, the Grid should have the following properties.

  • New elements make new columns
  • Each column is the width of the available space
  • Each column fills the remaining height of the page

We will set the width and the height of the body and the html elements so that they fill the viewport.

html,body {
width: 100%;
: 100%;
margin: 0;

Then we will layout our header our nav and our main using CSS Grid on the body

body {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: min-content min-content 1fr;

This makes the header and nav as small as possible ( min-content ) and gives the rest of the space to the main .

Now for the <article> elements. We will make another Grid on <main> to say they should all be laid out side by side each one taking up 100% of the width of the parent.

main {
display: grid;
grid-template-rows: 1fr;
grid-auto-flow: column;
grid-auto-columns: 100%;

We tell main to display with grid as before, we will set the the rows to take up all available space but we won’t set the number of columns since we could have any number of children. Instead we use grid-auto-flow: column; to tell it to add new columns when ever a new one has to be added and grid-auto-columns: 100%; to tell it that each new added column is 100% of the parent’s width.

Finally we will make it scroll by adding overflow-x: scroll; so that a scroll bar will always be present on that element and to allow it to scroll horizontally.

This is okay but not really usable. We want to be able to scroll just the main element and to have the panes snap into place as the user scrolls. Thank fully there is a CSS API for this: CSS Scroll Snap which has really good support across browsers.

To use it we configure the parent’s scroll-snap-type and add snap points to it’s descendants using scroll-snap-align .

main {
overflow-x: scroll;
scroll-snap-type: x mandatory;
main > article {
scroll-snap-align: start;
scroll-snap-stop: always;

We’ve made it scroll in the x direction, and set scroll-snap-type to mandatory so that it will always snap into place no matter where in the scroll it is.

Mandatory snapping can feel a little aggressive so you may choose to set it to proximity instead so that it only snaps near the edges, this may give a better experience on smaller screens but doesn’t guarantee lining up and the user can place the it such that the border between panes rests in the center of the screen.

Updated 09/23/2020: In some browsers the scrolling has a lot of momentum letting a quick flick scroll through many pages, which can be a frustrating user experience. To resolve this we use an experimental feature called scroll-snap-stop: always; so that it always stops on the next page. Greatly reducing the frustration whilst navigating between pages. [scroll-snap-stop on mdn]

This is almost ready, the deep links in the <nav> still work but the instantaneous jumping to the pages when a nav item is clicked is a poor user experience. To fix that we will add scroll-behavior: smooth; so that when we click on the nav links we get a smooth scroll to the selected page. This has no additional effect when the user is dragging with a finger or using a scroll wheel.

This demo is good, but can be polished a little more with some JavaScript, the next article adds a few additional features with Intersection Observer.

Try opening https://pink-paper.glitch.me in a mobile phone browser to try it with touch.

Samsung Internet Developers

Writings from the Samsung Internet Developer Relations…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store