HextaUI

Scroll fade

Edges of a scroll area that fade only when there's more to see, driven by the scroll position in CSS alone.

  • v2.24.0Released
  • v2.23.0Released
  • v2.22.0Released
  • v2.21.0Released
  • v2.20.0Released
  • v2.19.0Released
  • v2.18.0Released
  • v2.17.0Released
  • v2.16.0Released
  • v2.15.0Released
  • v2.14.0Released
  • v2.13.0Released
  • v2.12.0Released
  • v2.11.0Released
  • v2.10.0Released
  • v2.9.0Released
  • v2.8.0Released
  • v2.7.0Released
  • v2.6.0Released
  • v2.5.0Released
  • v2.4.0Released
  • v2.3.0Released
  • v2.2.0Released
  • v2.1.0Released
const releases = Array.from({ length: 24 }, (_, index) => `v2.${24 - index}.0`)

export function ScrollFadeDemo() {
  return (
    <div className="w-full max-w-xs rounded-xl bg-muted">
      <ul
        tabIndex={0}
        aria-label="Releases"
        className="h-64 scroll-fade overflow-y-auto rounded-xl p-2 text-sm outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
      >
        {releases.map((release) => (
          <li
            key={release}
            className="flex h-9 items-center justify-between rounded-md px-3"
          >
            <span className="font-mono">{release}</span>
            <span className="text-muted-foreground">Released</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

Scroll fade is a set of Tailwind utilities from shadcn's tailwind.css. The HextaUI theme already imports it, so if you've added the theme or any component, you have it.

pnpm dlx shadcn@latest add https://hextaui.com/r/theme.json

Without the theme, install shadcn and import its CSS after Tailwind.

pnpm add shadcn
app/globals.css
@import "tailwindcss";
@import "shadcn/tailwind.css";
<div className="scroll-fade h-64 overflow-y-auto">…</div>
<div className="flex scroll-fade-x overflow-x-auto">…</div>

Add it to any element that scrolls. An edge fades only while there is more content past it, so a cut-off row signals "keep scrolling" and a list at rest keeps crisp edges.

The fade is a mask-image, so content dissolves into whatever is behind it. There's no overlay gradient to match to the background, and it works on images, tinted surfaces and glass. A CSS scroll-driven animation grows each edge's fade over the first and last 96px of scrolling. No JavaScript runs, and it never re-renders.

  • Each fade is 12% of the container, capped at 40px.
  • Browsers without scroll-driven animations show both fades all the time. It's a little less precise, but still reads as scrollable.
  • scroll-fade-x, scroll-fade-s and scroll-fade-e follow the writing direction, so the start fade sits on the right in right-to-left layouts.

Horizontal

A row of filters that scrolls sideways. Pair it with no-scrollbar, also from shadcn's CSS, when the fade alone shows that there's more.

AllDesignEngineeringProductResearchMarketingSalesSupportOperationsFinance
const topics = [
  "All",
  "Design",
  "Engineering",
  "Product",
  "Research",
  "Marketing",
  "Sales",
  "Support",
  "Operations",
  "Finance",
]

export function ScrollFadeHorizontal() {
  return (
    <div
      tabIndex={0}
      role="region"
      aria-label="Topics"
      className="no-scrollbar flex w-full max-w-sm scroll-fade-x gap-2 overflow-x-auto rounded-md outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      {topics.map((topic) => (
        <span
          key={topic}
          className="shrink-0 rounded-full bg-muted px-3 py-1.5 text-sm"
        >
          {topic}
        </span>
      ))}
    </div>
  )
}

One edge

A chat starts at the bottom, so only older messages above need a hint. scroll-fade-t fades the top alone, and scroll-fade-t-16 makes it taller.

  • Sounds good, ship it.
  • Pushed the fix to main.
  • Pushed the fix to main.
  • Sounds good, ship it.
  • Pushed the fix to main.
  • Pushed the fix to main.
  • Sounds good, ship it.
  • Pushed the fix to main.
  • Pushed the fix to main.
  • Sounds good, ship it.
  • Pushed the fix to main.
  • Pushed the fix to main.
  • Sounds good, ship it.
  • Pushed the fix to main.
  • Pushed the fix to main.
  • Sounds good, ship it.
"use client"

const messages = Array.from({ length: 16 }, (_, index) => ({
  id: index,
  text: index % 3 === 0 ? "Sounds good, ship it." : "Pushed the fix to main.",
}))

export function ScrollFadeEdge() {
  return (
    <div className="w-full max-w-xs rounded-xl bg-muted">
      <ul
        ref={(node) => {
          if (node) {
            node.scrollTop = node.scrollHeight
          }
        }}
        tabIndex={0}
        aria-label="Messages"
        className="flex h-64 scroll-fade-t flex-col gap-2 overflow-y-auto rounded-xl p-3 text-sm outline-none scroll-fade-t-16 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
      >
        {messages.map((message) => (
          <li
            key={message.id}
            className="w-fit max-w-[80%] shrink-0 rounded-2xl bg-background px-3 py-2 even:self-end even:bg-primary even:text-primary-foreground"
          >
            {message.text}
          </li>
        ))}
      </ul>
    </div>
  )
}
<div className="scroll-fade scroll-fade-16 overflow-y-auto">…</div>
<div className="scroll-fade scroll-fade-[20%] overflow-y-auto">…</div>
<div className="scroll-fade [--scroll-fade-reveal:12rem] overflow-y-auto">…</div>

Sizes take the spacing scale or any length or percentage. --scroll-fade-reveal sets how far you scroll before an edge reaches its full fade.

  • The mask fades the element's own background and border too. Put the surface on a wrapper and the fade on the scrolling child, as the examples do.
  • A mask hides everything near the edge, including focus rings. Give the scrolling element some padding so focused items aren't faded out.
  • Use Scroll area instead when you also want custom scrollbars. It has its own fades, measured with JavaScript, which work in every browser.
ClassDescription
scroll-fade, scroll-fade-yFades the top and bottom edges.
scroll-fade-xFades the start and end edges, following direction.
scroll-fade-t, scroll-fade-bFades only the top or bottom edge.
scroll-fade-s, scroll-fade-eFades only the start or end edge.
scroll-fade-l, scroll-fade-rFades only the left or right edge, ignoring direction.
scroll-fade-<size>Size of every fade.
scroll-fade-{t,b,s,e}-<size>Size of one edge's fade.
scroll-fade-noneTurns the fade off, for example at a breakpoint.
--scroll-fade-revealScroll distance over which an edge fades in. Defaults to 96px.