Fade edges with a mask

Fade a moving edge with mask-image, not a hard overflow cut.

Emil Kowalski@emilkowalski
a simple overflow: hidden wouldn't work as it would break the animation. But today I had an epiphany that we could just use a simple CSS mask and fade the items out.
Aug 4, 2026View on X ↗

From emil-design-eng; demo 1 rebuilds Linear’s nested menu.

Nested menu: bleed, cut, fade

Open a submenu, or press Play and magnify.

No clippingBad
overflow: visible
overflow: hiddenBad
overflow: hidden
mask-imageGood
mask-image: linear-gradient(…)

Showing the root menu.

/* The clip lives on the viewport, not on the sliding panels. */
.menu-viewport {
  position: absolute;
  inset: 0;
  --fade: 14px; /* about the menu's side padding */
  mask-image: linear-gradient(
    to right,
    transparent,
    black var(--fade),
    black calc(100% - var(--fade)),
    transparent
  );
}

.menu-panel {
  transition: transform 300ms var(--ease-out);
}
.menu-panel[data-pos="left"]  { transform: translateX(-100%); }
.menu-panel[data-pos="right"] { transform: translateX(100%); }

Fade only the scrollable side

Scroll the strips sideways, or add a tab.

overflow-x: autoBad
mask-image: none
Mask, scrollable sideGood
--left 0px --right 32px

Selected: Overview.

.tabs {
  overflow-x: auto;
  mask-image: linear-gradient(
    to right,
    transparent 0,
    black var(--left, 0px),
    black calc(100% - var(--right, 0px)),
    transparent 100%
  );
}

tabs.addEventListener("scroll", () => {
  const max = tabs.scrollWidth - tabs.clientWidth;
  tabs.style.setProperty("--left", `${Math.min(tabs.scrollLeft, 32)}px`);
  tabs.style.setProperty("--right", `${Math.min(max - tabs.scrollLeft, 32)}px`);
}, { passive: true });

Why not a gradient overlay?

Switch the background, then scroll both lists.

Gradient overlayBad
    linear-gradient(transparent, var(--card))
    mask-imageGood
      linear-gradient(transparent, black)

      Background: dot grid.

      /* Overlay: only correct when the background is one flat color. */
      .list-wrap::after {
        background: linear-gradient(transparent, var(--card));
      }
      
      /* Mask: fades the content itself, whatever is behind it. */
      .list {
        overflow-y: auto;
        mask-image: linear-gradient(
          to bottom,
          transparent 0,
          black var(--top, 0px),
          black calc(100% - var(--bottom, 0px)),
          transparent 100%
        );
      }

      Why it matters, pros and cons

      Why it matters

      A hard edge slices glyphs mid-letter. A mask fades them before the edge.

      Pros

      • One declaration, no extra DOM.
      • Works over images, gradients and patterns.
      • A faded side hints "more this way".

      Cons

      • Shadows and focus rings near the edge fade too.
      • Fades content at rest; keep it inside the padding.
      • Nothing crosses the edge? Use text-overflow: ellipsis.