- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
<script setup lang="ts">
const items = Array.from({ length: 12 }, (_, index) => `Item ${index + 1}`)
</script>
<template>
<div class="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border">
<div class="scroll-fade scrollbar-none h-72 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
</template>Installation
If your project was set up with npx shadcn-vue@latest init, you already have scroll-fade. It ships with the shadcn-vue package, which the CLI imports in your global CSS file.
Otherwise, install the shadcn-vue package:
pnpm add shadcn-vue
Then import the shared utilities in your global CSS file:
@import "tailwindcss";
@import "shadcn-vue/tailwind.css";Usage
| Class | Styles |
|---|---|
scroll-fade | mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); animation-timeline: scroll(self y); |
scroll-fade-y | mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); animation-timeline: scroll(self y); |
scroll-fade-x | mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline)); animation-timeline: scroll(self inline); |
scroll-fade-t | Fade mask on the top edge. animation-timeline: scroll(self y); |
scroll-fade-b | Fade mask on the bottom edge. animation-timeline: scroll(self y); |
scroll-fade-l | Fade mask on the left edge. animation-timeline: scroll(self x); |
scroll-fade-r | Fade mask on the right edge. animation-timeline: scroll(self x); |
scroll-fade-s | Fade mask on the start edge, mirrors in RTL. animation-timeline: scroll(self inline); |
scroll-fade-e | Fade mask on the end edge, mirrors in RTL. animation-timeline: scroll(self inline); |
scroll-fade-<number> | --scroll-fade-size: calc(var(--spacing) * <number>); |
scroll-fade-[<value>] | --scroll-fade-size: <value>; |
scroll-fade-{t,b,s,e}-<number> | --scroll-fade-{t,b,s,e}-size: calc(var(--spacing) * <number>); |
scroll-fade-{t,b,s,e}-[<value>] | --scroll-fade-{t,b,s,e}-size: <value>; |
scroll-fade-none | --scroll-fade-mask: none; |
Add scroll-fade or scroll-fade-y to the scroll container, i.e. the element that has overflow-y-auto.
<template>
<div class="scroll-fade overflow-y-auto">
<!-- ... -->
</div>
</template>The fade is scroll-aware and tracks the scroll position:
- At rest, the top edge is crisp and the bottom edge fades to hint at more content.
- As you scroll, a fade appears at the top and both edges stay faded mid-scroll.
- At the end, the bottom edge sharpens to show you have reached the last item.
The fade is applied with mask-image, so it dissolves the content itself rather than overlaying a color. The mask uses a linear fade from transparent to black, so it adapts to any background without configuration. If your scroll area sits inside a card, put the background and border on a wrapper and scroll-fade on the inner scroller, so the fade dissolves the content and not the card.
The ScrollArea and MessageScroller components can use scroll-fade on their scrollable viewport.
No Overflow, No Fade
If the content does not overflow, no fade is shown. You can apply scroll-fade to any list without checking whether it scrolls.
<script setup lang="ts">
const items = Array.from({ length: 3 }, (_, index) => `Item ${index + 1}`)
</script>
<template>
<div class="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border">
<div class="scroll-fade scrollbar-none overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
</template>Horizontal Scrolling
Use scroll-fade-x on containers that scroll horizontally, i.e. the element that has overflow-x-auto.
<script setup lang="ts">
const tags = [
'Design',
'Engineering',
'Marketing',
'Product',
'Research',
'Sales',
'Support',
'Operations',
'Finance',
'Legal',
'People',
'Security',
]
</script>
<template>
<div class="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border">
<div class="scroll-fade-x scrollbar-none overflow-x-auto">
<div class="flex w-max gap-1.5 p-1.5">
<div
v-for="tag in tags"
:key="tag"
class="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ tag }}
</div>
</div>
</div>
</div>
</template><template>
<div class="flex scroll-fade-x overflow-x-auto">
<!-- ... -->
</div>
</template>The horizontal fade is direction-aware. In RTL layouts, the crisp edge and the fade follow the reading direction with no extra classes needed. scroll-fade-<number> and scroll-fade-none work the same for both axes.
Edge Fades
Use edge utilities when only one edge should track the scroll position.
scroll-fade-t
scroll-fade-b
scroll-fade-s
scroll-fade-e
<script setup lang="ts">
const items = [
'Inbox triage',
'Design review',
'API contract',
'QA pass',
'Launch notes',
'Metrics follow-up',
]
const tags = [
'Design',
'Engineering',
'Marketing',
'Product',
'Research',
'Sales',
'Support',
'Operations',
]
</script>
<template>
<div class="mx-auto flex max-w-xs min-w-0 flex-col gap-6">
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-t scrollbar-none h-36 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-t
</p>
</div>
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-b scrollbar-none h-36 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-b
</p>
</div>
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-s scrollbar-none overflow-x-auto">
<div class="flex w-max gap-1.5 p-1.5">
<div
v-for="tag in tags"
:key="tag"
class="shrink-0 rounded-xl bg-muted px-4 py-2.5 text-sm"
>
{{ tag }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-s
</p>
</div>
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-e scrollbar-none overflow-x-auto">
<div class="flex w-max gap-1.5 p-1.5">
<div
v-for="tag in tags"
:key="tag"
class="shrink-0 rounded-xl bg-muted px-4 py-2.5 text-sm"
>
{{ tag }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-e
</p>
</div>
</div>
</template><template>
<div class="scroll-fade-b overflow-y-auto">
<!-- ... -->
</div>
</template>The edge utilities are scroll-aware. Start edges fade in after you scroll away from the start, and end edges fade out when you reach the end. Use scroll-fade-t, scroll-fade-b, scroll-fade-l, and scroll-fade-r for physical edges. Use scroll-fade-s and scroll-fade-e for logical inline edges that mirror in RTL.
Fade Size
The fade depth defaults to 12% of the container, capped at 40px so tall scrollers stay subtle. Use scroll-fade-<number> to set a fixed size on the spacing scale instead, the same way scroll-mt-<number> works.
scroll-fade-4
scroll-fade-24
<script setup lang="ts">
const items = Array.from({ length: 8 }, (_, index) => `Item ${index + 1}`)
</script>
<template>
<div class="mx-auto flex w-full max-w-xs flex-col gap-6">
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade scroll-fade-4 scrollbar-none h-48 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-4
</p>
</div>
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade scroll-fade-24 scrollbar-none h-48 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade-24
</p>
</div>
</div>
</template><template>
<div class="scroll-fade scroll-fade-24 overflow-y-auto">
<!-- ... -->
</div>
</template>For one-off values, use an arbitrary length or percentage:
<template>
<div class="scroll-fade scroll-fade-[15%] overflow-y-auto">
<!-- ... -->
</div>
</template>To fade opposite edges by different amounts, use the per-edge modifiers scroll-fade-t-<number>, scroll-fade-b-<number>, scroll-fade-s-<number>, and scroll-fade-e-<number>. They override scroll-fade-<number> on the edge they target and accept arbitrary values too.
<template>
<div class="scroll-fade scroll-fade-b-8 scroll-fade-t-2 overflow-y-auto">
<!-- ... -->
</div>
</template>Use the logical s/e modifiers for horizontal scrollers so the sizes mirror in RTL.
The fade eases in and out over a fixed scroll distance rather than appearing instantly. That distance is the --scroll-fade-reveal variable, 96px by default and independent of the fade depth. Lower it for a snappier reveal or raise it for a more gradual one:
<template>
<div class="scroll-fade overflow-y-auto [--scroll-fade-reveal:64px]">
<!-- ... -->
</div>
</template>Disabling the Fade
Use scroll-fade-none to remove the fade. It works in any class order, so the typical use is responsive or stateful:
<template>
<div class="scroll-fade overflow-y-auto md:scroll-fade-none">
<!-- ... -->
</div>
</template>scroll-fade
scroll-fade scroll-fade-none
<script setup lang="ts">
const items = Array.from({ length: 8 }, (_, index) => `Item ${index + 1}`)
</script>
<template>
<div class="mx-auto flex w-full max-w-xs min-w-0 flex-col gap-6">
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade scrollbar-none h-48 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade
</p>
</div>
<div class="flex flex-col gap-3">
<div class="overflow-hidden rounded-2xl border">
<div class="scroll-fade scroll-fade-none scrollbar-none h-48 overflow-y-auto">
<div class="flex flex-col gap-1.5 p-1.5">
<div
v-for="item in items"
:key="item"
class="rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ item }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
scroll-fade scroll-fade-none
</p>
</div>
</div>
</template>Fallback
The scroll-aware behavior is implemented with CSS scroll-driven animations, with no JavaScript and no scroll listeners. In browsers that do not support scroll-driven animations, scroll-fade falls back to a static fade on both edges, and edge utilities fall back to a static fade on the selected edge.
Since the mask is applied to the scroll container itself, a visible scrollbar fades with the content at the edges. Pair scroll-fade with scrollbar-none, which ships in the same package, if you want to hide the scrollbar entirely.
RTL
scroll-fade-x follows the reading direction. At rest, the start edge is crisp and the end edge fades. In RTL layouts that means a crisp right edge and a fade on the left, mirrored from LTR.
dir="ltr"
dir="rtl"
<script setup lang="ts">
const ltrTags = [
'Design',
'Engineering',
'Marketing',
'Product',
'Research',
'Sales',
'Support',
'Operations',
'Finance',
'Legal',
]
const rtlTags = [
'تصميم',
'هندسة',
'تسويق',
'منتج',
'أبحاث',
'مبيعات',
'دعم',
'عمليات',
'مالية',
'قانوني',
]
</script>
<template>
<div class="mx-auto grid w-full max-w-lg gap-6 sm:grid-cols-2">
<div class="flex flex-col gap-3">
<div dir="ltr" class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-x scrollbar-none overflow-x-auto">
<div class="flex w-max gap-1.5 p-1.5">
<div
v-for="tag in ltrTags"
:key="tag"
class="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ tag }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
dir="ltr"
</p>
</div>
<div class="flex flex-col gap-3">
<div dir="rtl" class="overflow-hidden rounded-2xl border">
<div class="scroll-fade-x scrollbar-none overflow-x-auto">
<div class="flex w-max gap-1.5 p-1.5">
<div
v-for="tag in rtlTags"
:key="tag"
class="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{{ tag }}
</div>
</div>
</div>
</div>
<p class="text-center font-mono text-xs text-muted-foreground">
dir="rtl"
</p>
</div>
</div>
</template>