How do I add a top and bottom shadow while scrolling but only when needed?

Solution 1:

I think your looking for something like this;

Reference : LINK

html {
  background: white;
  font: 120% sans-serif;
}

.scrollbox {
  overflow: auto;
  width: 200px;
  max-height: 200px;
  margin: 50px auto;
  background: /* Shadow covers */
  linear-gradient(white 30%, rgba(255, 255, 255, 0)), linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, /* Shadows */
  radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%;
  background: /* Shadow covers */
  linear-gradient(white 30%, rgba(255, 255, 255, 0)), linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, /* Shadows */
  radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%;
  background-repeat: no-repeat;
  background-color: white;
  background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
  /* Opera doesn't support this in the shorthand */
  background-attachment: local, local, scroll, scroll;
}
<div class="scrollbox">
  <ul>
    <li>I Show Below Shadow. Go Down Now</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
    <li>11</li>
    <li>12</li>
    <li>13</li>
    <li>14</li>
    <li>15</li>
    <li>16</li>
    <li>17</li>
    <li>18</li>
    <li>19</li>
    <li>20</li>
    <li>The end!</li>
    <li>No shadow here. See Above. Go Up</li>
  </ul>
</div>

Solution 2:

I apologize for answering my own question here but after some googling, I found a good CSS-only solution that works in chrome on macOS.

@Hash provided a solution taken from lea verou's blog. Her solution uses background-attachment: local to achieve the effect however this is currently broken in chrome 59 on macOS??

It worked in 2012 but not in 2017 for me right now at least


It works in 2020 now so you can look at the accepted answer instead of this one. 🤷‍♀️


Nevertheless, her solution is great and you should read more about it here.

In her article (from 2012) she refers to an older yet more compatible solution that uses pseudo elements. This blog post is by Roman Komarov and here are some direct quotes from his article.

Here’s an old idea, but recre­ated with pure CSS.

Orig­i­nally, I had an ex­tra wrap­per and two ex­tra pseudo-el­e­ments on it. Later I de­cided to rewrite the code and to use just a sin­gle el­e­ment (by us­ing ra­dial gra­di­ents).

While this method is sim­ple, there are some lim­i­ta­tions:

  • the back­ground must be solid
    • how­ever, if you'd try back­ground-at­tach­ment: fixed…)
  • there are some po­si­tion­ing is­sues

But in most re­gards this method is rather bul­let­proof.

If you re­place the CSS-gra­di­ents with sim­ple im­ages, this method could work in IE. (It might need a few more small fixes; I didn't check.)

And here is some code copied directly from his blog:

html {
  background: #FFF;
}

.scrollbox {
  position: relative;
  z-index: 1;
  overflow: auto;
  width: 200px;
  max-height: 200px;
  margin: 50px auto;
  background: #FFF no-repeat;
  background-image: -webkit-radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), -webkit-radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));
  background-image: -moz-radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), -moz-radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));
  background-image: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));
  background-position: 0 0, 0 100%;
  background-size: 100% 14px;
}

.scrollbox:before,
.scrollbox:after {
  content: "";
  position: relative;
  z-index: -1;
  display: block;
  height: 30px;
  margin: 0 0 -30px;
  background: -webkit-linear-gradient(top, #FFF, #FFF 30%, rgba(255, 255, 255, 0));
  background: -moz-linear-gradient(top, #FFF, #FFF 30%, rgba(255, 255, 255, 0));
  background: linear-gradient(to bottom, #FFF, #FFF 30%, rgba(255, 255, 255, 0));
}

.scrollbox:after {
  margin: -30px 0 0;
  background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), #FFF 70%, #FFF);
  background: -moz-linear-gradient(top, rgba(255, 255, 255, 0), #FFF 70%, #FFF);
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #FFF 70%, #FFF);
}
<div class="scrollbox">
    <ul>
        <li>Not enough content to scroll</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>
</div>


<div class="scrollbox">
    <ul>
        <li>Ah! Scroll below!</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        <li>7</li>
        <li>8</li>
        <li>9</li>
        <li>10</li>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        <li>7</li>
        <li>8</li>
        <li>9</li>
        <li>10</li>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        <li>7</li>
        <li>8</li>
        <li>The end!</li>
        <li>No shadow there.</li>
    </ul>
</div>

Solution 3:

Here's a pure CSS solution I put together that puts the shadow over the content instead of behind it.

/* Stuff that doesn't matter */

html {
  font-size: 16px;
}

body {
  margin: 0;
  padding: 0;
  font: 1rem/1.5 arial, sans-serif;
}

.c-container {
  width: 100%;
  max-width: 400px;
  margin: 0 auto;
  padding: 0 2rem;
  box-sizing: border-box;
}

.c-header {
  width: 100%;
  padding: 1rem 0;
  background: white;
}

.c-header_heading {
  margin: 0;
  font-size: 2rem;
  font-weight: bold;
}

/* Stuff that matters */

.c-scrollbox {
  position: relative;
  width: 100%;
  height: 200px;
  overflow: scroll;
}

.c-scrollbox::before {
  content: '';
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  display: block;
  height: 1px;
  background: rgba(0,0,0,.32);
}

.c-scrollbox::after {
  content: '';
  position: absolute;
  top: 0;
  display: block;
  width: 100%;
  height: 1rem;
  background: linear-gradient(rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%);
}
<div class="c-container">
  <header class="c-header">
    <h1 class="c-header_heading">Shadow on Scroll</h1>
  </header>
  <div class="c-scrollbox">
    <p>Maecenas faucibus mollis interdum. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ullamcorper nulla non metus auctor fringilla. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Cras justo odio, dapibus ac facilisis in, egestas eget quam.</p>
    <p>Donec id elit non mi porta gravida at eget metus. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Sed posuere consectetur est at lobortis. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Etiam porta sem malesuada magna mollis euismod. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
    <p>Vestibulum id ligula porta felis euismod semper. Nullam quis risus eget urna mollis ornare vel eu leo. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Nullam quis risus eget urna mollis ornare vel eu leo. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.</p>
    <p>Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec id elit non mi porta gravida at eget metus. Nulla vitae elit libero, a pharetra augue. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Cras justo odio, dapibus ac facilisis in, egestas eget quam.</p>
  </div>
</div>

Solution 4:

The CSS-only solution in the accepted answer works great if it is okay for your use case that the shadows are positioned behind the content, as part of the element’s background.

If your use case requires the shadows to be above the content, or independent of the element’s background, you likely need to use a bit of JavaScript. I had these requirements, and built a custom element that works exactly like the OP asked: <scroll-shadow> element.

Example usage (works vertical and horizontal, see demo here):

<scroll-shadow>
  <nav>Content…</nav>
</scroll-shadow>

The OP asked for a CSS-only solution, which this isn’t. Still, it leverages CSS and shouldn’t have a negative impact on performance. Posting this as an answer because from the other comments, it seems that there is a general need to have the shadows above the content.