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-size | 40px | 控制渐变阴影尺寸。该值由 size prop 设置。 |
--scroll-shadow-offset | 0px | 开始显示渐变之前需要滚动的距离。该值由 offset prop 设置。 |
--scroll-shadow-scrollbar-size | 10px(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" | 阴影视觉效果样式 |
size | number | 40 | 阴影渐变尺寸(像素) |
offset | number | 0 | 开始显示阴影前的滚动偏移量(像素) |
hideScrollBar | boolean | false | 是否隐藏原生滚动条 |
isEnabled | boolean | true | 是否启用滚动阴影检测 |
visibility | "auto" | "both" | "top" | "bottom" | "left" | "right" | "none" | "auto" | 受控的阴影可见性 |
onVisibilityChange | (visibility: ScrollShadowVisibility) => void | - | 阴影可见性变化时调用的回调 |
className | string | - | 应用到根元素上的额外 CSS 类 |
children | ReactNode | - | 可滚动的子内容 |