Skip to content
Lenso UI

ScrollShadow 滚动阴影

通过阴影提示可滚动溢出内容,并根据滚动位置自动检测显示或隐藏。

用法

import { ScrollShadow } from "@lenso/ui";

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
import { ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  scroll: { maxHeight: 240, padding: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },});export function Default() {  return (    <div {...stylex.props(styles.root)}>      <ScrollShadow tabIndex={0} aria-label="Scrollable sample text" xstyle={styles.scroll}>        <div {...stylex.props(styles.paragraphs)}>          {Array.from({ length: 10 }, (_, index) => (            <p key={index}>              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non              risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.              Morbi accumsan cursus enim, sed ultricies sapien.            </p>          ))}        </div>      </ScrollShadow>    </div>  );}export default Default;

Local adaptation source above. Derived from HeroUI v3.2.6 source.

示例

方向

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Card, ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const images = [  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/robot1.jpeg",  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/avocado.jpeg",  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/oranges.jpeg",];const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  vertical: { marginBottom: 32, width: "100%" },  heading: { marginBottom: 8, fontSize: 14, lineHeight: "20px", fontWeight: 600 },  card: { width: "100%", padding: 0 },  verticalScroll: { maxHeight: 240, padding: 16 },  horizontalScroll: { padding: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },  cards: { display: "flex", flexDirection: "row", gap: 16 },  item: { display: "flex", minWidth: 200, flexDirection: "row", gap: 12, padding: 4 },  image: {    aspectRatio: "1",    width: { default: 64, "@media (min-width: 640px)": 80 },    height: { default: 64, "@media (min-width: 640px)": 80 },    flexShrink: 0,    borderRadius: "var(--radius-xl)",    objectFit: "cover",    userSelect: "none",  },  text: { display: "flex", flex: 1, flexDirection: "column", justifyContent: "center", gap: 4 },  title: { fontSize: 14 },  description: { fontSize: 12 },});export default function Orientation() {  return (    <div {...stylex.props(styles.root)}>      <div {...stylex.props(styles.vertical)}>        <h4 {...stylex.props(styles.heading)}>Vertical</h4>        <Card xstyle={styles.card}>          <ScrollShadow            tabIndex={0}            aria-label="Vertical sample"            xstyle={styles.verticalScroll}            orientation="vertical"          >            <div {...stylex.props(styles.paragraphs)}>              {Array.from({ length: 10 }, (_, index) => (                <p key={index}>                  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non                  risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor                  quam. Morbi accumsan cursus enim, sed ultricies sapien.                </p>              ))}            </div>          </ScrollShadow>        </Card>      </div>      <div>        <h4 {...stylex.props(styles.heading)}>Horizontal</h4>        <Card xstyle={styles.card}>          <ScrollShadow            tabIndex={0}            aria-label="Horizontal cards"            xstyle={styles.horizontalScroll}            orientation="horizontal"          >            <div {...stylex.props(styles.cards)}>              {Array.from({ length: 10 }, (_, index) => (                <Card key={index} xstyle={styles.item} variant="transparent">                  <img                    alt="Lorem Card"                    {...stylex.props(styles.image)}                    loading="lazy"                    src={images[index % images.length]}                  />                  <div {...stylex.props(styles.text)}>                    <Card.Title xstyle={styles.title}>Bridging the Future</Card.Title>                    <Card.Description xstyle={styles.description}>Today, 6:30 PM</Card.Description>                  </div>                </Card>              ))}            </div>          </ScrollShadow>        </Card>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

阴影尺寸

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  scroll: { maxHeight: 240, padding: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },});export default function CustomSize() {  return (    <div {...stylex.props(styles.root)}>      <ScrollShadow        size={80}        tabIndex={0}        aria-label="Scrollable sample text"        xstyle={styles.scroll}      >        <div {...stylex.props(styles.paragraphs)}>          {Array.from({ length: 10 }, (_, index) => (            <p key={index}>              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non              risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.              Morbi accumsan cursus enim, sed ultricies sapien.            </p>          ))}        </div>      </ScrollShadow>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

与 Card 组合

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Button, Card, ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  card: { maxWidth: 400 },  content: { padding: 0 },  scroll: { height: 300, paddingInline: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },  footer: { marginTop: 16, display: "flex", flexDirection: "row", gap: 8 },  button: { width: "100%" },});export default function WithCard() {  return (    <Card xstyle={styles.card}>      <Card.Header>        <Card.Title>Terms and Conditions</Card.Title>        <Card.Description>Please review before proceeding</Card.Description>      </Card.Header>      <Card.Content xstyle={styles.content}>        <ScrollShadow          tabIndex={0}          aria-label="Terms and Conditions"          xstyle={styles.scroll}          size={80}        >          <div {...stylex.props(styles.paragraphs)}>            {Array.from({ length: 10 }, (_, index) => (              <p key={index}>                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non                risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor                quam. Morbi accumsan cursus enim, sed ultricies sapien.              </p>            ))}          </div>        </ScrollShadow>      </Card.Content>      <Card.Footer xstyle={styles.footer}>        <Button xstyle={styles.button} variant="secondary">          Cancel        </Button>        <Button xstyle={styles.button}>Accept</Button>      </Card.Footer>    </Card>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

隐藏滚动条

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  scroll: { maxHeight: 240, padding: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },});export default function HideScrollBar() {  return (    <div {...stylex.props(styles.root)}>      <ScrollShadow        hideScrollBar        tabIndex={0}        aria-label="Scrollable sample text"        xstyle={styles.scroll}      >        <div {...stylex.props(styles.paragraphs)}>          {Array.from({ length: 10 }, (_, index) => (            <p key={index}>              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non              risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.              Morbi accumsan cursus enim, sed ultricies sapien.            </p>          ))}        </div>      </ScrollShadow>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

可见性变化

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Card, ScrollShadow, type ScrollShadowVisibility } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";const images = [  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/robot1.jpeg",  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/avocado.jpeg",  "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/oranges.jpeg",];const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  section: { display: "flex", flexDirection: "column", gap: 8 },  vertical: { marginBottom: 32 },  status: { borderRadius: "var(--radius)", backgroundColor: "var(--default)", padding: 16 },  statusText: { fontSize: 14, lineHeight: "20px", fontWeight: 600 },  verticalScroll: { maxHeight: 240, padding: 16 },  horizontalScroll: { padding: 16 },  paragraphs: { display: "flex", flexDirection: "column", gap: 16 },  cards: { display: "flex", flexDirection: "row", gap: 16 },  item: { display: "flex", minWidth: 200, flexDirection: "row", gap: 12, padding: 4 },  image: {    aspectRatio: "1",    width: { default: 64, "@media (min-width: 640px)": 80 },    height: { default: 64, "@media (min-width: 640px)": 80 },    flexShrink: 0,    borderRadius: "var(--radius-xl)",    objectFit: "cover",    userSelect: "none",  },  text: { display: "flex", flex: 1, flexDirection: "column", justifyContent: "center", gap: 4 },  title: { fontSize: 14 },  description: { fontSize: 12 },});export default function VisibilityChange() {  const [verticalState, setVerticalState] = useState<ScrollShadowVisibility>("none");  const [horizontalState, setHorizontalState] = useState<ScrollShadowVisibility>("none");  return (    <div {...stylex.props(styles.root)}>      <div {...stylex.props(styles.section, styles.vertical)}>        <div {...stylex.props(styles.status)}>          <p {...stylex.props(styles.statusText)}>Vertical Shadow State: {verticalState}</p>        </div>        <ScrollShadow          tabIndex={0}          aria-label="Vertical sample"          xstyle={styles.verticalScroll}          orientation="vertical"          onVisibilityChange={setVerticalState}        >          <div {...stylex.props(styles.paragraphs)}>            {Array.from({ length: 10 }, (_, index) => (              <p key={index}>                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non                risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor                quam. Morbi accumsan cursus enim, sed ultricies sapien.              </p>            ))}          </div>        </ScrollShadow>      </div>      <div {...stylex.props(styles.section)}>        <div {...stylex.props(styles.status)}>          <p {...stylex.props(styles.statusText)}>Horizontal Shadow State: {horizontalState}</p>        </div>        <ScrollShadow          tabIndex={0}          aria-label="Horizontal cards"          xstyle={styles.horizontalScroll}          orientation="horizontal"          onVisibilityChange={setHorizontalState}        >          <div {...stylex.props(styles.cards)}>            {Array.from({ length: 10 }, (_, index) => (              <Card key={index} xstyle={styles.item} variant="transparent">                <img                  alt="Lorem Card"                  {...stylex.props(styles.image)}                  loading="lazy"                  src={images[index % images.length]}                />                <div {...stylex.props(styles.text)}>                  <Card.Title xstyle={styles.title}>Bridging the Future</Card.Title>                  <Card.Description xstyle={styles.description}>Today, 6:30 PM</Card.Description>                </div>              </Card>            ))}          </div>        </ScrollShadow>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

自定义样式

Tailwind CSS

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const entries = [  "Reviewed quarterly goals with the design team.",  "Shipped dark mode tokens to production.",  "Merged accessibility fixes for form fields.",  "Published updated component documentation.",  "Scheduled performance audit for next sprint.",  "Added scroll shadow demos to the docs site.",];const styles = stylex.create({  root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } },  scroll: {    maxHeight: 192,    borderRadius: "var(--radius-xl)",    border: "1px solid color-mix(in oklab, var(--border) 80%, transparent)",    backgroundImage:      "linear-gradient(to bottom, light-dark(oklch(98.5% 0 0 / .9), oklch(20.5% 0 0 / .8)), light-dark(white, oklch(20.5% 0 0)))",    padding: 16,    boxShadow: "0 0 0 1px light-dark(rgb(0 0 0 / .05), rgb(255 255 255 / .1))",  },  entries: { display: "flex", flexDirection: "column", gap: 12 },  entry: {    fontSize: 14,    lineHeight: 1.625,    color: "light-dark(oklch(43.9% 0 0), oklch(70.8% 0 0))",  },});export function CustomStyles() {  return (    <div {...stylex.props(styles.root)}>      <ScrollShadow        hideScrollBar        tabIndex={0}        aria-label="Recent activity"        xstyle={styles.scroll}        size={48}        variant="fade"      >        <div {...stylex.props(styles.entries)}>          {entries.map((entry) => (            <p key={entry} {...stylex.props(styles.entry)}>              {entry}            </p>          ))}        </div>      </ScrollShadow>    </div>  );}export default CustomStyles;

Local adaptation source above. Derived from HeroUI v3.2.6 source.

全局 CSS

若要自定义组件类,可使用 @layer components 指令。了解更多。

@layer components {  .scroll-shadow {    @apply rounded-xl border border-default-200;  }
  .scroll-shadow--vertical {    @apply pr-2; /* Add padding for custom scrollbar styling */  }
  .scroll-shadow--horizontal {    @apply pb-2;  }}

样式参考

HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。

CSS 类

ScrollShadow 组件使用以下 CSS 类(查看源码样式):

基础类 [!toc]

  • .scroll-shadow - 根容器元素

方向变体 [!toc]

  • .scroll-shadow--vertical - 纵向滚动(默认)
  • .scroll-shadow--horizontal - 横向滚动

状态修饰符 [!toc]

  • .scroll-shadow--hide-scrollbar - 隐藏原生滚动条

CSS 变量

ScrollShadow 组件使用 CSS 变量设置渐变遮罩尺寸,并为可见的原生滚动条保留空间:

变量默认值描述
--scroll-shadow-size40px控制渐变阴影尺寸。该值由 size prop 设置。
--scroll-shadow-offset0px开始显示渐变之前需要滚动的距离。该值由 offset prop 设置。
--scroll-shadow-scrollbar-size10px(hideScrollBar 时为 0px)为原生滚动条保留一段实色遮罩区域,避免渐变覆盖滚动条。使用更宽的自定义滚动条时可以覆盖该值。

Data 属性

组件使用 data 属性控制阴影可见性:

  • 滚动状态:[data-top-scroll]、[data-bottom-scroll]、[data-left-scroll]、[data-right-scroll] — 当内容可向对应方向滚动时应用
  • 组合状态:[data-top-bottom-scroll]、[data-left-right-scroll] — 当内容可向两个方向滚动时应用
  • 方向:[data-orientation="vertical"] 或 [data-orientation="horizontal"] — 表示滚动方向
  • 尺寸:[data-scroll-shadow-size] — 阴影渐变尺寸数值
  • 阴影模式:[data-scroll-shadow-mode] — 渐变由滚动位置推导时为 "auto";visibility 受控或 isEnabled 为 false 时为 "manual"

滚动驱动的渐变

在 auto 模式下,支持滚动驱动动画的浏览器会直接在 CSS 中根据滚动位置推导渐变。因此遮罩在首次绘制时就是正确的,无需测量,也不会在 hydration 期间出现未渐变内容的闪烁。 不支持的浏览器会回退到上面的 [data-*-scroll] 属性,这些属性在 hydration 之后才写入。自定义样式时需要注意两点:

  • 在 auto 模式下,即使没有可滚动内容,根元素也始终会解析出 mask-image。这会使其成为层叠上下文, 并成为 position: fixed 后代元素的包含块。如需退出该行为,请显式设置 visibility。
  • 滚动驱动的渐变依赖根元素上的 animation 属性。在同一元素上使用 animate-* 工具类会覆盖它,导致渐变消失。 请改为对外层容器应用动画。

API 参考

ScrollShadow

Prop类型默认值描述
orientation"vertical" | "horizontal""vertical"滚动方向
variant"fade""fade"阴影视觉效果样式
sizenumber40阴影渐变尺寸(像素)
offsetnumber0开始显示阴影前的滚动偏移量(像素)
hideScrollBarbooleanfalse是否隐藏原生滚动条
isEnabledbooleantrue是否启用滚动阴影检测
visibility"auto" | "both" | "top" | "bottom" | "left" | "right" | "none""auto"受控的阴影可见性
onVisibilityChange(visibility: ScrollShadowVisibility) => void-阴影可见性变化时调用的回调
classNamestring-应用到根元素上的额外 CSS 类
childrenReactNode-可滚动的子内容

相关组件