Solution: Auto-scrolling, responsive grid

Front-End Challenges Club - Solution #006


This is the solution for Challenge #006.

Modern CSS allows us to seriously improve our layouts and today’s solution really throws the modern CSS kitchen sink at layout. What you’ll get at the end is a solid layout solution that—thanks to progressive enhancement—will work all the way back to old browsers.

This solution has the following code files:

  • index.html
  • css/global.css

You can see a live demo or download a complete version of what we’re making in this solution, here.

HTML permalink

As always, let’s start with a nice HTML shell. Open up your index.html file and add the following:

Code language
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Front-End Challenges Club #006</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/modern-css-reset/dist/reset.min.css"
    />
    <link rel="stylesheet" href="/css/global.css" />
  </head>
  <body>
    <main></main>
  </body>
</html>

This is our core structure and as you can see, it’s pretty simple. We’re pulling in our CSS, along with a CDN version of my modern reset that I use.

Add the following HTML between the <main> tags:

Code language
html
<div class="wrapper">
  <div class="reel-grid">
    <ol class="reel-grid__list">
      <li>
        <span class="reel-grid__cell">Item #001</span>
      </li>
      <!-- Add 7 more list elements here -->
    </ol>
  </div>
</div>

We’ve got a list here called .reel-grid which is our main component. It resides in a .wrapper which limits the width and centers itself. This is not a required element.

You’ll also notice that there’s a HTML comment to add 7 more list items. Go ahead and do that and then we are done with HTML.

CSS permalink

Let’s start our CSS by adding our custom properties. Add the following to global.css:

Code language
css
:root {
  --color-dark: #353535;
  --color-light: #ffffff;
  --color-mid: #efefef;
  --color-slate: #7f7f7f;
  --metric-gutter: 1.5rem;
  --metric-shadow-width: 0.6rem;
}

These are the same as the challenge, but I’ve added --metic-gutter and --metric-shadow-width to keep things nice and consistent and readable, because magic numbers can be a touch confusing.

After the custom properties, add the following to your CSS:

Code language
css
body {
  font-family: sans-serif;
  color: var(--color-dark);
  padding: 4rem 1.5rem;
}

.wrapper {
  max-width: 66rem;
  margin: 0 auto;
}

The global styles are short and sweet with this one. All I’m doing is making stuff look a bit nicer and then adding that limited width wrapper.

Let’s plug on to the good stuff.

Reel-grid component

After your global CSS, add the following:

Code language
css
.reel-grid {
  padding: 0 0 var(--metric-gutter) 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: auto;
}

This is our default experience. Because we will set our items side-by-side, we need to set our overflow to auto. The -webkit-overflow-scrolling adds inertia support to iOS devices, so the scrolling is all nice and bouncy.

Let’s add some custom scrollbars now:

Code language
css
.reel-grid::-webkit-scrollbar {
  height: 1rem;
}

.reel-grid::-webkit-scrollbar-track {
  background-color: var(--color-mid);
}

.reel-grid::-webkit-scrollbar-thumb {
  background-color: var(--color-slate);
}

We’re using -webkit prefixed pseudo elements here because right now, it’s only feasible to create custom scrollbars with CSS with those, and you know what, I’m cool with that. Custom scrollbars are very much a progressive enhancement.

Let’s add the default layout now:

Code language
css
.reel-grid__list {
  display: flex;
}

.reel-grid__list > * {
  min-width: 14rem;
}

.reel-grid__list > * + * {
  margin-inline-start: var(--metric-gutter);
}

Our .reel-grid__list element is set as a flex container. After that, we’re setting a sensible min-width which will make items (mostly) equal in width (in this context). You certainly don’t need to add it if you don’t want to!

Lastly, we’re using a logical property to set a space between items. This means that if the direction of the page was switched to rtl, the margin would also switch.

With that, our default layout is done!

Shadow tricks and grid

Get buckled into you seat because we’re digging into the details now!

We’re going to add a shadow to the each side of the layout if there are items concealed by the overflow rule. I’m going to use Lea Verou’s legendary trick as the basis for this and expand it to work with modern CSS.

Add the following CSS:

Code language
css
@supports (mix-blend-mode: multiply) {
  .reel-grid {
    background: linear-gradient(90deg, #fff 30%, rgba(255, 255, 255, 0)),
      linear-gradient(90deg, rgba(255, 255, 255, 0), #fff 70%) 0 100%, radial-gradient(
        farthest-side at 0 50%,
        rgba(0, 0, 0, 0.3),
        transparent
      ), radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.3), transparent) 0 100%;
    background-repeat: no-repeat;
    background-size: var(--metric-gutter) 100%, var(--metric-gutter) 100%,
      var(--metric-shadow-width) calc(100% - var(--metric-gutter)), var(
          --metric-shadow-width
        ) calc(100% - var(--metric-gutter));
    background-position: left top, 100%, left top, right top;
    background-attachment: local, local, scroll, scroll;
  }

  .reel-grid__cell {
    mix-blend-mode: multiply;
  }
}

There is a lot to digest here, so let’s break it down:

  1. First of all, we wrap everything in an @supports block because for this to work how I want it to work, we need mix-blend-mode.
  2. We add 4 gradients. The first two should be seen as “masks” that conceal the shadows. These are simple white-to-transparent linear gradients. What follows them are two radial gradients that sit on both sides. These are our shadows.
  3. We set the size of the masks first to be as wide as our gutter and 100% high. Then we set our shadows to use their --metric-shadow-width property, which should be smaller than your gutter.
  4. Next up: magic time. Using background-position and background-attachment we make our masks and shadows behave how we want them to. Because the masks are local, they affix themselves to the edges of our .reel-grid__list. Then, because our shadows are scroll, they only show when there is scrolling to be done, because of the masks. Magic, right?

I’ll again link up to Lea’s trick because this one has been in my back-pocket for years, and has been super handy. The only issue though, is that because we use backgrounds for this, they sit behind our items. This is where blend modes come in.

Using mix-blend-mode: multiply on our reel-grid__cell elements essentially removes the white out of them, making that transparent, but importantly, the text remains readable. It’s a trick we designers have been doing with Photoshop for ever! This is why we wrapped it all in a supports block, too, because without that, the shadows are pretty useless.

That’s the shadows done, so let’s quickly style our cells so you can see all of this come together. Add the following CSS:

Code language
css
.reel-grid__cell {
  display: block;
  padding: 4rem;
  background: var(--color-mid);
  border: 1px solid var(--color-slate);
  font-weight: bold;
  text-align: center;
}

Refresh your browser now and you’ll see it all working together. Pretty neat, right?

Expanding the grid

The last part of this solution is the grid expansion. Add the following CSS:

Code language
css
@media (min-width: 65em) {
  @supports (display: grid) {
    .reel-grid {
      padding: 0;
      margin: 0;
      background: none;
    }

    .reel-grid__list {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      grid-gap: var(--metric-gutter);
    }

    .reel-grid__list > * + * {
      margin-inline-start: 0;
    }
  }
}

We fence the grid expansion in a media-query and an @supports. This is because if the screen isn’t big enough and there’s no CSS Grid support, then we might as well leave our current default as it is.

Inside the two blocks, we set a pretty straightforward 4 column grid, by using grid-template-columns: repeat(4, 1fr), which tells the grid to repeatedly split the space between 4 elements, equally.

Finally, we remove that margin that we added to space items and with that, we are done!!

Wrapping up permalink

I really hope you’ve enjoyed this tutorial. You can grab a zip of the final code that I wrote and also see a live version too!

My favourite attempt for this challenge was again, by Dana Byerly. Dana’s attempt is so very close to what we did here and the attempt at the shadows is solid. As always, their attention to detail blows my mind.