ScrollShadow
Apply visual shadows to indicate scrollable content overflow with automatic detection of scroll position.
Usage
<script setup lang="ts">
import { ScrollShadow } from '@hareui/vue'
</script><script setup lang="ts">import { ScrollShadow } from '@hareui/vue'</script><template> <div class="w-full p-0 sm:max-w-sm"> <ScrollShadow class="max-h-[240px] p-4"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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></template>Examples
Orientation
Vertical
Horizontal
<script setup lang="ts">import { Card, ScrollShadow } from '@hareui/vue'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 getRandomImage = (idx: number) => images[idx % images.length]</script><template> <div class="w-full sm:max-w-sm"> <div class="mb-8 w-full"> <h4 class="mb-2 text-sm font-semibold"> Vertical </h4> <Card class="w-full p-0"> <ScrollShadow class="max-h-[240px] p-4" orientation="vertical"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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 class="w-full"> <h4 class="mb-2 text-sm font-semibold"> Horizontal </h4> <Card class="w-full p-0"> <ScrollShadow class="p-4" orientation="horizontal"> <div class="flex flex-row gap-4"> <Card v-for="idx in 10" :key="`scroll-shadow-lorem-cards-${idx}`" class="flex min-w-[200px] flex-row gap-3 p-1" :ui="{ content: 'justify-center' }" variant="transparent" > <template #leading> <img alt="Lorem Card" class="aspect-square h-16 w-16 shrink-0 rounded-xl object-cover select-none sm:h-20 sm:w-20" loading="lazy" :src="getRandomImage(idx - 1)" > </template> <h3 class="text-sm leading-6 font-medium text-foreground"> Bridging the Future </h3> <p class="text-xs leading-5 text-muted"> Today, 6:30 PM </p> </Card> </div> </ScrollShadow> </Card> </div> </div></template>Shadow Size
<script setup lang="ts">import { ScrollShadow } from '@hareui/vue'</script><template> <div class="w-full p-0 sm:max-w-sm"> <ScrollShadow class="max-h-[240px] p-4" :size="80"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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></template>With Card
Terms and Conditions
Please review before proceeding
<script setup lang="ts">import { Button, Card, ScrollShadow } from '@hareui/vue'</script><template> <Card class="max-w-[400px]" description="Please review before proceeding" title="Terms and Conditions" :ui="{ content: 'p-0', footer: 'mt-4 flex flex-row gap-2' }" > <ScrollShadow class="h-[300px] px-4" :size="80"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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> <template #footer> <Button class="w-full" variant="secondary"> Cancel </Button> <Button class="w-full"> Accept </Button> </template> </Card></template>Hide Scroll Bar
<script setup lang="ts">import { ScrollShadow } from '@hareui/vue'</script><template> <div class="w-full p-0 sm:max-w-sm"> <ScrollShadow hide-scroll-bar class="max-h-[240px] p-4"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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></template>Visibility Change
Vertical Shadow State: none
Horizontal Shadow State: none
<script setup lang="ts">import type { ScrollShadowVisibility } from '@hareui/vue'import { Card, ScrollShadow } from '@hareui/vue'import { ref } from 'vue'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 getRandomImage = (idx: number) => images[idx % images.length]const verticalState = ref<ScrollShadowVisibility>('none')const horizontalState = ref<ScrollShadowVisibility>('none')</script><template> <div class="w-full sm:max-w-sm"> <div class="mb-8 flex flex-col gap-2"> <div class="rounded bg-default p-4"> <p class="text-sm font-semibold"> Vertical Shadow State: {{ verticalState }} </p> </div> <div class="w-full"> <ScrollShadow class="max-h-[240px] p-4" orientation="vertical" @visibility-change="verticalState = $event"> <div class="space-y-4"> <p v-for="idx in 10" :key="`scroll-shadow-lorem-content-${idx}`"> 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> <div class="flex flex-col gap-2"> <div class="rounded bg-default p-4"> <p class="text-sm font-semibold"> Horizontal Shadow State: {{ horizontalState }} </p> </div> <div class="w-full"> <ScrollShadow class="p-4" orientation="horizontal" @visibility-change="horizontalState = $event"> <div class="flex flex-row gap-4"> <Card v-for="idx in 10" :key="`scroll-shadow-lorem-cards-${idx}`" class="flex min-w-[200px] flex-row gap-3 p-1" :ui="{ content: 'justify-center' }" variant="transparent" > <template #leading> <img alt="Lorem Card" class="aspect-square h-16 w-16 shrink-0 rounded-xl object-cover select-none sm:h-20 sm:w-20" loading="lazy" :src="getRandomImage(idx - 1)" > </template> <h3 class="text-sm leading-6 font-medium text-foreground"> Bridging the Future </h3> <p class="text-xs leading-5 text-muted"> Today, 6:30 PM </p> </Card> </div> </ScrollShadow> </div> </div> </div></template>Customization
Tailwind CSS
<script setup lang="ts">import { ScrollShadow } from '@hareui/vue'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.',]</script><template> <div class="w-full sm:max-w-sm"> <ScrollShadow hide-scroll-bar class="max-h-48 rounded-xl border border-border/80 bg-linear-to-b from-neutral-50/90 to-white p-4 ring-1 ring-black/5 dark:from-neutral-900/80 dark:to-neutral-900 dark:ring-white/10" :size="48" variant="fade" > <div class="space-y-3"> <p v-for="entry in entries" :key="entry" class="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400" > {{ entry }} </p> </div> </ScrollShadow> </div></template>Global Configuration
To customize the ScrollShadow classes for the whole app, extend the scrollShadow theme in createHareUI:
app.use(createHareUI({
ui: {
scrollShadow: {
slots: { base: 'rounded-xl border border-default-200' },
variants: {
orientation: {
vertical: { base: 'pr-2' }, // Add padding for custom scrollbar styling
horizontal: { base: 'pb-2' },
},
},
},
},
}))Styling Reference
Slots
base→[data-slot="scroll-shadow"]– root container element
Variants
orientation="vertical"– vertical scrolling (default)orientation="horizontal"– horizontal scrollinghide-scroll-bar– hides the native scrollbar
The fade masks live in HareUI's stylesheet (styles/scroll-shadow.css) and key on [data-slot="scroll-shadow"] and [data-orientation], so they apply to any element carrying those attributes (Tabs' scrolling list uses the same component).
CSS Variables
The ScrollShadow component uses CSS variables to size the fade mask and reserve space for visible native scrollbars:
| Variable | Default | Description |
|---|---|---|
--scroll-shadow-size | 40px | Controls the fade gradient size. This is set from the size prop. |
--scroll-shadow-offset | 0px | How far the container must be scrolled before the fade starts. This is set from the offset prop. |
--scroll-shadow-scrollbar-size | 10px (0px when hideScrollBar) | Reserves a solid mask gutter for the native scrollbar so the fade does not cover it. Override for wider scrollbars. |
Data Attributes
The component uses data attributes to control shadow visibility:
- Scroll States:
[data-top-scroll],[data-bottom-scroll],[data-left-scroll],[data-right-scroll]- Applied when content can be scrolled in that direction - Combined States:
[data-top-bottom-scroll],[data-left-right-scroll]- Applied when content can be scrolled in both directions - Orientation:
[data-orientation="vertical"]or[data-orientation="horizontal"]- Indicates scroll direction - Size:
[data-scroll-shadow-size]- Contains the shadow gradient size value - Shadow Mode:
[data-scroll-shadow-mode]-"auto"when the fade is derived from the scroll position,"manual"whenvisibilityis controlled orenabledisfalse
Scroll-Driven Fade
In auto mode, browsers that support scroll-driven animations derive the fade from the scroll position in CSS. The mask is therefore correct on the very first paint, with no measurement and no flash of unfaded content during hydration. Browsers without support fall back to the [data-*-scroll] attributes above, which are written after mount. Two things to keep in mind when customizing:
- In
automode the root always resolves amask-image, even when there is nothing to scroll. That makes it a stacking context and a containing block forposition: fixeddescendants. Setvisibilityexplicitly if you need to opt out. - The scroll-driven fade uses the
animationproperty on the root. Applying ananimate-*utility to the same element replaces it and leaves no fade. Animate a wrapper instead.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
as | AsTag | Component | "div" | The element or component to render the root as. |
size | number | 40 | The shadow size in pixels. |
offset | number | 0 | The scroll offset (px) before the shadow appears. |
orientation | "horizontal" | "vertical" | "vertical" | The scroll orientation. |
variant | "fade" | "fade" | The visual variant. |
hideScrollBar | boolean | false | Whether to hide the scrollbar. |
visibility | ScrollShadowVisibility | "auto" | Which shadows to show. auto follows the scroll position; anything else forces the
shadows (manual mode). |
enabled | boolean | true | Whether scroll detection is enabled (HeroUI's isEnabled). |
ui | ComponentSlots<{ slots: { base: string; }; variants: { orientation: { vertical: { base: string; }; horizontal: { base: string; }; }; hideScrollBar: { false: { base: string; }; true: { base: string; }; }; variant: { fade: {}; }; }; defaultVariants: { orientation: string; hideScrollBar: boolean; variant: string; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
default | any | The scrollable content. |
Emits
| Event | Payload | Description |
|---|---|---|
visibilityChange | [visibility: ScrollShadowVisibility] | - |


