Navigation

v0.0.4
Structure

Scroll area

A scrolling region whose edges fade where content continues.

On this page

A scroll area is a region that scrolls inside the page: a long list in a panel, a wide table, a log. It keeps the platform's own scrolling, with a thin quiet scrollbar, and adds one thing: where content continues past an edge, that edge fades out, and where the content ends, the edge is clean.

Scroll area

<div class="sheet max-w(360) mx-auto">  <div class="scroll-area h(200) p-md text-sm fg-text-body leading-md" tabindex="0" role="region" aria-label="Release notes">    <p class="m(0)">2.1 adds overlays that need no script.</p>    <p class="m(0) mt-sm">Dialogs open with invoker commands and trap focus on their own.</p>    <p class="m(0) mt-sm">Popovers, menus and tooltips are placed with anchor positioning and flip at the viewport edge.</p>    <p class="m(0) mt-sm">The select draws its option list in the same material where the browser allows it.</p>    <p class="m(0) mt-sm">Segmented controls and tabs glide one indicator between options.</p>    <p class="m(0) mt-sm">Corners turn with continuous curvature where corner-shape is supported.</p>    <p class="m(0) mt-sm">Every one of these falls back to a working component.</p>  </div></div>
Scroll the notes: the top edge fades in once there is content above it, and the bottom edge clears at the end.

#How the fade knows

The fade is a mask whose two stops are custom properties, --scroll-area-start and --scroll-area-end. Two animations on the region's own scroll timeline drive them: one runs over the first 1.5rem of scrolling and opens the fade at the start edge, the other runs over the last 1.5rem and closes the fade at the end edge. There is no script and there are no sentinel elements. While nothing overflows, the timeline is inactive and both edges stay clean.

In a browser without scroll-driven animations the region scrolls the same and has no fades.

#Along the inline axis

scroll-area-inline scrolls sideways and fades the start and end edges instead: a row of cards, or a table wider than its column.

Along the inline axis

<div class="scroll-area scroll-area-inline" tabindex="0" role="region" aria-label="Regions">  <div class="flex gap-sm w-fit py(4) px(2)">    <span class="chip">Tokyo</span>    <span class="chip">Singapore</span>    <span class="chip">Sydney</span>    <span class="chip">Mumbai</span>    <span class="chip">Frankfurt</span>    <span class="chip">London</span>    <span class="chip">Paris</span>    <span class="chip">Stockholm</span>    <span class="chip">Virginia</span>    <span class="chip">Oregon</span>    <span class="chip">São Paulo</span>    <span class="chip">Cape Town</span>  </div></div>

#Accessibility

  • A region that scrolls must be reachable by the keys. Give it tabindex="0", a role such as region and a label, unless it already holds focusable content.
  • The mask fades the region's own scrollbar at the same edges. In forced colors the mask is removed.
  • Give the region a height or a maximum height; without one it grows with its content and never scrolls.

#Classes

ClassDescription
scroll-areaA region that scrolls along the block axis, with a thin scrollbar and edges that fade where content continues
scroll-area-inlineScrolls along the inline axis and fades the start and end edges. With scroll-area

#Custom properties

PropertyDefaultDescription
--scroll-area-fade1.5remLength of the fade at each edge

#Source

@master/ui/components/scroll-area.css
/* * Scroll area: a region that scrolls inside the page, with a thin quiet scrollbar. * Where content continues past an edge, that edge fades out; where it ends, the edge is clean. The fade is * read from the scroll position itself by a scroll-driven animation, with no script and no sentinel elements. * Without scroll-driven animations the region scrolls the same and its edges stay clean. */@property --scroll-area-start {  syntax: '<length>';  inherits: false;  initial-value: 0px;}@property --scroll-area-end {  syntax: '<length>';  inherits: false;  initial-value: 0px;}@layer components {  .scroll-area {    --scroll-area-fade: 1.5rem;    min-width: 0;    min-height: 0;    overflow: auto;    overscroll-behavior: contain;    scrollbar-width: thin;    scrollbar-color: var(--color-line-strong) transparent;    &:focus-visible {      @apply --focus-ring;    }    /* Scrolls along the inline axis: a row of cards, a wide table. */    &.scroll-area-inline {      overflow: auto hidden;    }    /*     * Two animations on the region's own scroll timeline. The first runs over the first stretch of scrolling     * and opens the fade at the start edge; the second runs over the last stretch and closes the fade at the     * end edge. While nothing overflows the timeline is inactive and both lengths stay 0.     */    @supports (animation-timeline: scroll()) {      mask-image: linear-gradient(to bottom, transparent, black var(--scroll-area-start), black calc(100% - var(--scroll-area-end)), transparent);      /* Longhands: tools that fold the timeline into the shorthand write a value no browser reads. */      animation-name: scroll-area-start, scroll-area-end;      animation-timing-function: linear;      animation-fill-mode: both;      animation-timeline: scroll(self block), scroll(self block);      animation-range: 0 var(--scroll-area-fade), calc(100% - var(--scroll-area-fade)) 100%;      &.scroll-area-inline {        mask-image: linear-gradient(to right, transparent, black var(--scroll-area-start), black calc(100% - var(--scroll-area-end)), transparent);        animation-timeline: scroll(self inline), scroll(self inline);      }    }    @media (forced-colors: active) {      scrollbar-color: auto;      mask-image: none;    }  }}@keyframes scroll-area-start {  from {    --scroll-area-start: 0px;  }  to {    --scroll-area-start: var(--scroll-area-fade);  }}@keyframes scroll-area-end {  from {    --scroll-area-end: var(--scroll-area-fade);  }  to {    --scroll-area-end: 0px;  }}