Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
edef460
Scroll-area initial
mikenewbon Oct 16, 2025
613fffe
Add images and fix horizontal gap/padding
mikenewbon Oct 16, 2025
66fbefd
set defaults
mikenewbon Oct 16, 2025
d3a678e
improve performance
mikenewbon Oct 17, 2025
0c2aab8
fix non-virtual, add item theme slot
mikenewbon Oct 17, 2025
4edbfb8
add responsive layout support with RTL option and dynamic lane calcul…
mikenewbon Oct 17, 2025
96d16f9
Fix RTL
mikenewbon Oct 18, 2025
24e782c
fix theme
mikenewbon Oct 18, 2025
bb2edb6
Update spec
mikenewbon Oct 18, 2025
5490f00
Fix types
mikenewbon Oct 18, 2025
2a3f763
Fix vue spec
mikenewbon Oct 18, 2025
209c8f5
remove notes
mikenewbon Oct 18, 2025
63af0e4
Update docs, fix horizontal scroll
mikenewbon Oct 19, 2025
0b80ce9
Expose events and methods
mikenewbon Oct 19, 2025
04ed186
add virtualizer props type import and simplify options with spread
mikenewbon Oct 19, 2025
8cfcab6
fix type issues
mikenewbon Oct 19, 2025
b815c84
Revert spread
mikenewbon Oct 19, 2025
76a1762
improve docs
mikenewbon Oct 19, 2025
ca9b286
Fix type imports
mikenewbon Oct 19, 2025
e1aff11
Improve docs
mikenewbon Oct 19, 2025
b4b8db2
Add ScrollArea to BlogPosts
mikenewbon Oct 19, 2025
ad0343b
Add infite scrolling
mikenewbon Oct 19, 2025
67af1bd
remove changes to component example
mikenewbon Oct 19, 2025
c329065
minor fixes
mikenewbon Oct 27, 2025
b020857
Fix lint error
mikenewbon Oct 28, 2025
896cb36
Remove Blog posts from PR
mikenewbon Nov 5, 2025
5cf6829
Update Nuxt dependencies to version 4.2.0 and refactor BlogPosts comp…
mikenewbon Nov 5, 2025
21ada29
and lock
mikenewbon Nov 5, 2025
92b34c2
loadMoreThreshold option for infinite scrolling, fix reactivity for i…
mikenewbon Nov 6, 2025
1f43b42
simplify!!
mikenewbon Nov 12, 2025
db26e7d
remove some examples
mikenewbon Nov 12, 2025
14cec5a
fix non-virtual playground example and orientation and theme
mikenewbon Nov 13, 2025
774346d
Fix tests
mikenewbon Nov 13, 2025
e51e6a2
Merge branch 'v4' into pr/5245
benjamincanac Nov 13, 2025
c6b0e0a
chore: revert pnpm lock
benjamincanac Nov 13, 2025
d2b5cb2
docs: add images
benjamincanac Nov 13, 2025
6a63a6e
clean code + add `data-slot` attrs + fix ts errors
benjamincanac Nov 13, 2025
526a0df
chore(Table/ScrollArea): add tsdoc link to `virtualize` prop
benjamincanac Nov 13, 2025
75cf17c
fix(ScrollArea): dont expose rootRef
benjamincanac Nov 13, 2025
490c1b6
chroe(theme): clean duplicated class
benjamincanac Nov 13, 2025
73fa583
playground: improve
benjamincanac Nov 13, 2025
5b2739b
chore(theme): improve
benjamincanac Nov 13, 2025
c56d483
docs: update
benjamincanac Nov 13, 2025
44d15bf
rename to match docs and add infinite scroll example
mikenewbon Nov 14, 2025
0959749
Shorten description
mikenewbon Nov 14, 2025
cc879d7
fix horizontal layouts
mikenewbon Nov 14, 2025
0c8af61
update default spacing
mikenewbon Nov 17, 2025
89f5116
remove (px)
mikenewbon Nov 17, 2025
a6971d1
remove defaults from examples
mikenewbon Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/app/components/content/ComponentExample.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const props = withDefaults(defineProps<{
* A list of variable props to link to the component.
*/
options?: Array<{
type?: string
alias?: string
name: string
label: string
Expand Down Expand Up @@ -195,6 +196,7 @@ const urlSearchParams = computed(() => {
<UInput
v-else
:model-value="get(optionsValues, option.name)"
:type="option.type"
color="neutral"
variant="soft"
:ui="{ base: 'rounded-sm rounded-l-none min-w-12' }"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<UScrollArea class="h-96 w-full border border-default rounded-lg">
<UCard>
<template #header>
<h3 class="font-semibold">
Section 1
</h3>
</template>
<p>Custom content without using the items prop.</p>
</UCard>
<UCard>
<template #header>
<h3 class="font-semibold">
Section 2
</h3>
</template>
<p>Any content can be placed here and it will be scrollable.</p>
</UCard>
<UCard>
<template #header>
<h3 class="font-semibold">
Section 3
</h3>
</template>
<p>You can mix different components and layouts as needed.</p>
</UCard>
</UScrollArea>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
orientation?: 'vertical' | 'horizontal'
virtualize?: boolean
lanes?: number
gap?: number
padding?: number
}>()

const items = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`,
description: `Description for item ${i + 1}`
}))
</script>

<template>
<UScrollArea
v-slot="{ item }"
:items="items"
:orientation="orientation"
:virtualize="virtualize ? {
lanes: lanes && lanes > 1 ? lanes : undefined,
gap,
paddingStart: padding,
paddingEnd: padding
} : false"
class="h-96 w-full border border-default rounded-lg"
>
<UCard class="h-full overflow-hidden">
<template #header>
<h3 class="font-semibold">
{{ item.title }}
</h3>
</template>
<p class="text-sm text-muted">
{{ item.description }}
</p>
</UCard>
</UScrollArea>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { UButton } from '#components'

type Recipe = {
id: number
name: string
image: string
difficulty: string
cuisine: string
rating: number
reviewCount: number
prepTimeMinutes: number
cookTimeMinutes: number
}

type RecipeResponse = {
recipes: Recipe[]
total: number
skip: number
limit: number
}

const skip = ref(0)
const limit = 10

const { data, status, execute } = await useFetch('https://dummyjson.com/recipes?limit=10&select=name,image,difficulty,cuisine,rating,reviewCount,prepTimeMinutes,cookTimeMinutes', {
key: 'scroll-area-recipes-infinite-scroll',
params: { skip, limit },
transform: (data?: RecipeResponse) => {
return data?.recipes
},
lazy: true,
immediate: false
})

const recipes = ref<Recipe[]>([])

watch(data, () => {
if (data.value) {
recipes.value = [...recipes.value, ...data.value]
}
})

execute()

function loadMore() {
if (status.value !== 'pending') {
skip.value += limit
}
}
</script>

<template>
<UScrollArea
:items="recipes"
:virtualize="{
estimateSize: 120,
loadMoreThreshold: 5
}"
class="h-96 w-full"
@load-more="loadMore"
>
<template #default="{ item }">
<UPageCard :description="`${item.cuisine} β€’ ${item.difficulty}`" orientation="horizontal" :ui="{ container: 'lg:flex flex-row' }">
<template #header>
<UUser
:name="item.name"
:description="`${item.prepTimeMinutes + item.cookTimeMinutes} min β€’ ${item.reviewCount} reviews`"
:avatar="{ src: item.image, alt: item.name }"
/>
</template>
<UButton color="neutral" variant="subtle" size="xl" class="fit-content justify-self-end">
<UIcon name="i-lucide-star" class="size-3" />
{{ item.rating }}
</UButton>
</UPageCard>
</template>
</UScrollArea>

<UIcon v-if="status === 'pending'" name="i-lucide-loader-circle" class="animate-spin size-5 absolute bottom-4 left-0 right-0 mx-auto" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
defineProps<{
orientation?: 'vertical' | 'horizontal'
}>()

const items = Array.from({ length: 30 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`,
description: `Description for item ${i + 1}`
}))
</script>

<template>
<UScrollArea
v-slot="{ item }"
:items="items"
:orientation="orientation"
:class="orientation === 'vertical' ? 'h-96 flex flex-col' : 'w-full'"
class="border border-default rounded-lg"
>
<UCard>
<template #header>
<h3 class="font-semibold">
{{ item.title }}
</h3>
</template>
<p class="text-sm text-muted">
{{ item.description }}
</p>
</UCard>
</UScrollArea>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script setup lang="ts">
const props = defineProps<{
targetIndex?: number
itemCount?: number
}>()

const items = computed(() => Array.from({ length: props.itemCount || 1000 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`
})))

const scrollArea = useTemplateRef('scrollArea')

function scrollToTop() {
scrollArea.value?.scrollToIndex(0, { align: 'start', behavior: 'smooth' })
}

function scrollToBottom() {
scrollArea.value?.scrollToIndex(items.value.length - 1, { align: 'end', behavior: 'smooth' })
}

function scrollToItem(index: number) {
scrollArea.value?.scrollToIndex(index - 1, { align: 'center', behavior: 'smooth' })
}
</script>

<template>
<div class="space-y-4 w-full">
<UScrollArea
ref="scrollArea"
:items="items"
:virtualize="{ estimateSize: 58 }"
class="h-96 w-full border border-default rounded-lg p-4"
>
<template #default="{ item, index }">
<div
class="p-3 mb-2 rounded-lg border border-default"
:class="index === (targetIndex || 500) - 1 ? 'bg-primary-500/10 border-primary-500/20' : 'bg-elevated'"
>
<span class="font-medium">{{ item.title }}</span>
</div>
</template>
</UScrollArea>

<UFieldGroup size="sm">
<UButton icon="i-lucide-arrow-up-to-line" color="neutral" variant="outline" @click="scrollToTop">
Top
</UButton>
<UButton icon="i-lucide-arrow-down-to-line" color="neutral" variant="outline" @click="scrollToBottom">
Bottom
</UButton>
<UButton icon="i-lucide-navigation" color="neutral" variant="outline" @click="scrollToItem(targetIndex || 500)">
Go to {{ targetIndex || 500 }}
</UButton>
</UFieldGroup>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
const items = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
title: `Card ${i + 1}`,
description: i % 3 === 0
? `This is a longer description with more text to demonstrate variable height handling in virtualized lists. Item ${i + 1} has significantly more content than others.`
: `Short description for item ${i + 1}.`
}))
</script>

<template>
<UScrollArea
v-slot="{ item }"
:items="items"
:virtualize="{ estimateSize: 120, lanes: 3 }"
class="h-96 w-full border border-default rounded-lg"
>
<UCard>
<template #header>
<h3 class="font-semibold">
{{ item.title }}
</h3>
</template>
<p class="text-sm text-muted">
{{ item.description }}
</p>
</UCard>
</UScrollArea>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
const props = defineProps<{
itemCount?: number
}>()

const items = computed(() => Array.from({ length: props.itemCount || 10000 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`,
description: `Description for item ${i + 1}`
})))
</script>

<template>
<UScrollArea
v-slot="{ item }"
:items="items"
virtualize
class="h-96 w-full border border-default rounded-lg p-4"
>
<UCard class="mb-4">
<template #header>
<h3 class="font-semibold">
{{ item.title }}
</h3>
</template>
<p class="text-sm text-muted">
{{ item.description }}
</p>
</UCard>
</UScrollArea>
</template>
Loading
Loading