Scroll Area
Augments native scroll functionality for custom, cross-browser styling.
Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
dir | ltr | ltr , rtl | The reading direction of the combobox when applicable. If omitted, inherits globally from ConfigProvider or assumes LTR (left-to-right) reading mode. |
scrollHideDelay | 600 | number | Determines the length of time, in milliseconds, before the scrollbars are hidden |
type | hover | scroll , always , auto , hover | Describes the nature of scrollbar visibility, similar to how the scrollbar preferences in MacOS control visibility of native scrollbars. |
Tags
<script setup lang="ts">
const tags = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`,
)
</script>
<template>
<div class="grid w-full place-items-center">
<NScrollArea class="h-72 w-80 border rounded-md">
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Tags
</h4>
<div v-for="el in tags" :key="el">
<div class="text-sm">
{{ el }}
</div>
<NSeparator v-if="el !== tags[tags.length - 1]" class="my-2" />
</div>
</div>
</NScrollArea>
</div>
</template>
Orientation
The orientation
prop controls the scroll direction and can be set on either the ScrollArea
component or via _scrollAreaScrollbar
.
Prop | Default | Type | Description |
---|---|---|---|
orientation | vertical | vertical , horizontal | The orientation of the scrollbar |
<script setup lang="ts">
interface Artwork {
id: string
artist: string
art: string
}
const works: Artwork[] = [
{
id: '1',
artist: 'Ornella Binni',
art: 'https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?auto=format&fit=crop&w=300&q=80',
},
{
id: '2',
artist: 'Tom Byrom',
art: 'https://images.unsplash.com/photo-1548516173-3cabfa4607e9?auto=format&fit=crop&w=300&q=80',
},
{
id: '3',
artist: 'Vladimir Malyavko',
art: 'https://images.unsplash.com/photo-1494337480532-3725c85fd2ab?auto=format&fit=crop&w=300&q=80',
},
]
</script>
<template>
<div class="grid w-full place-items-center">
<NScrollArea orientation="horizontal" class="h-72 w-96 border rounded-md">
<div class="w-max flex p-4 space-x-4">
<div v-for="artwork in works" :key="artwork.id">
<figure class="shrink-0">
<div class="overflow-hidden rounded-md">
<img
:src="artwork.art"
:alt="`Photo by ${artwork.artist}`"
class="aspect-[3/4] h-56 w-36 object-cover"
>
</div>
<figcaption class="text-muted-foreground pt-2 text-xs">
Photo by
<span class="text-foreground font-semibold">
{{ artwork.artist }}
</span>
</figcaption>
</figure>
</div>
</div>
</NScrollArea>
</div>
</template>
Color
Prop | Default | Type | Description |
---|---|---|---|
scrollArea | gray | string | The color of the scroll area. |
You can use any color palette you want. Una UI uses Tailwind CSS Colors under the hood, But you can also define your own custom theme colors, see Extending Section.
scroll-area="primary"
Notifications
New message
You have a new message from Sarah
2 min agoMeeting reminder
Team meeting in 30 minutes
10 min agoTask completed
Project X has been marked as complete
1 hour agoNew sign-up
New user registered: John Doe
3 hours agoSystem update
System will be updated at midnight
5 hours agoscroll-area="pink"
Comments
Sarah Johnson
Great progress on the project!
2 hours agoMichael Chen
I think we should reconsider the approach for the homepage.
3 hours agoEmily Rodriguez
The new design looks amazing!
5 hours agoDavid Kim
When is the next release scheduled?
1 day agoAlex Turner
I found a small bug in the navigation menu when testing on mobile.
2 days agoscroll-area="lime"
Tasks
Update documentation
HighFix navigation bug
CriticalReview pull requests
MediumPrepare for release
HighTeam meeting notes
Lowscroll-area="orange"
Events
Team Meeting
Product Launch
Client Presentation
Workshop
Company Retreat
<script setup lang="ts">
const notifications = [
{ id: 1, title: 'New message', description: 'You have a new message from Sarah', time: '2 min ago', read: false },
{ id: 2, title: 'Meeting reminder', description: 'Team meeting in 30 minutes', time: '10 min ago', read: false },
{ id: 3, title: 'Task completed', description: 'Project X has been marked as complete', time: '1 hour ago', read: true },
{ id: 4, title: 'New sign-up', description: 'New user registered: John Doe', time: '3 hours ago', read: true },
{ id: 5, title: 'System update', description: 'System will be updated at midnight', time: '5 hours ago', read: true },
]
const comments = [
{ id: 1, author: 'Sarah Johnson', text: 'Great progress on the project!', time: '2 hours ago' },
{ id: 2, author: 'Michael Chen', text: 'I think we should reconsider the approach for the homepage.', time: '3 hours ago' },
{ id: 3, author: 'Emily Rodriguez', text: 'The new design looks amazing!', time: '5 hours ago' },
{ id: 4, author: 'David Kim', text: 'When is the next release scheduled?', time: '1 day ago' },
{ id: 5, author: 'Alex Turner', text: 'I found a small bug in the navigation menu when testing on mobile.', time: '2 days ago' },
]
const tasks = [
{ id: 1, title: 'Update documentation', completed: false, priority: 'High' },
{ id: 2, title: 'Fix navigation bug', completed: true, priority: 'Critical' },
{ id: 3, title: 'Review pull requests', completed: false, priority: 'Medium' },
{ id: 4, title: 'Prepare for release', completed: false, priority: 'High' },
{ id: 5, title: 'Team meeting notes', completed: true, priority: 'Low' },
]
const events = [
{ id: 1, title: 'Team Meeting', date: 'Today, 2:00 PM', location: 'Conference Room A' },
{ id: 2, title: 'Product Launch', date: 'Tomorrow, 10:00 AM', location: 'Main Hall' },
{ id: 3, title: 'Client Presentation', date: 'Jan 15, 3:30 PM', location: 'Meeting Room B' },
{ id: 4, title: 'Workshop', date: 'Jan 20, 9:00 AM', location: 'Training Center' },
{ id: 5, title: 'Company Retreat', date: 'Feb 5-7', location: 'Mountain Resort' },
]
</script>
<template>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 md:grid-cols-2">
<!-- Primary - Notifications -->
<div class="flex flex-col items-center">
<h5 class="mb-2 text-sm font-medium">
scroll-area="primary"
</h5>
<NScrollArea
type="always"
scroll-area="primary"
class="h-64 w-full border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Notifications
</h4>
<div v-for="item in notifications" :key="item.id" class="mb-3">
<div class="flex items-start">
<div class="flex-1">
<h5 class="font-medium">
{{ item.title }}
</h5>
<p class="text-sm opacity-80">
{{ item.description }}
</p>
<span class="text-xs opacity-60">{{ item.time }}</span>
</div>
<div class="mt-1 h-2 w-2 rounded-full" :class="item.read ? 'bg-gray-300' : 'bg-primary'" />
</div>
<NSeparator v-if="item.id !== notifications[notifications.length - 1].id" class="my-3" />
</div>
</div>
</NScrollArea>
</div>
<!-- Secondary - Comments -->
<div class="flex flex-col items-center">
<h5 class="mb-2 text-sm font-medium">
scroll-area="pink"
</h5>
<NScrollArea
type="always"
scroll-area="pink"
class="h-64 w-full border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Comments
</h4>
<div v-for="comment in comments" :key="comment.id" class="mb-3">
<div>
<h5 class="font-medium">
{{ comment.author }}
</h5>
<p class="text-sm">
{{ comment.text }}
</p>
<span class="text-xs opacity-60">{{ comment.time }}</span>
</div>
<NSeparator v-if="comment.id !== comments[comments.length - 1].id" class="my-3" />
</div>
</div>
</NScrollArea>
</div>
<!-- lime - Tasks -->
<div class="flex flex-col items-center">
<h5 class="mb-2 text-sm font-medium">
scroll-area="lime"
</h5>
<NScrollArea
type="always"
scroll-area="lime"
class="h-64 w-full border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Tasks
</h4>
<div v-for="task in tasks" :key="task.id" class="mb-3">
<div class="flex items-center">
<NCheckbox
class="mr-2"
:model-value="task.completed"
/>
<div class="flex-1">
<p class="text-sm" :class="task.completed ? 'line-through opacity-70' : ''">
{{ task.title }}
</p>
<span
class="text-xs"
:class="task.priority === 'Critical' ? 'text-error' : task.priority === 'High' ? 'text-warning' : 'text-muted'"
>
{{ task.priority }}
</span>
</div>
</div>
<NSeparator v-if="task.id !== tasks[tasks.length - 1].id" class="my-3" />
</div>
</div>
</NScrollArea>
</div>
<!-- Orange - Events -->
<div class="flex flex-col items-center">
<h5 class="mb-2 text-sm font-medium">
scroll-area="orange"
</h5>
<NScrollArea
type="always"
scroll-area="orange"
class="h-64 w-full border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Events
</h4>
<div v-for="event in events" :key="event.id" class="mb-3">
<div>
<h5 class="font-medium">
{{ event.title }}
</h5>
<div class="flex items-center text-sm space-x-2">
<span>📅 {{ event.date }}</span>
<span>•</span>
<span>📍 {{ event.location }}</span>
</div>
</div>
<NSeparator v-if="event.id !== events[events.length - 1].id" class="my-3" />
</div>
</div>
</NScrollArea>
</div>
</div>
</template>
Size
Prop | Default | Type | Description |
---|---|---|---|
size | md | string | The size of the scroll area. |
You can you use any number size that you can imagine, breakpoints (e.g., sm:sm, xs:lg
), or states (e.g., hover:lg, focus:3xl
).
size="10px"
10 Pixel
size="sm"
Small
size="0.75cm"
0.75 cm
<script setup lang="ts">
// Create consistent content for all size examples
const contentItems = Array.from({ length: 15 }).map(
(_, i) => `Item ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
)
// Different sizes to demonstrate with custom labels
const sizes = [
{ size: '10px', label: '10 Pixel' },
{ size: 'sm', label: 'Small' },
{ size: '0.75cm', label: '0.75 cm' },
]
</script>
<template>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 sm:grid-cols-2">
<div v-for="item in sizes" :key="item.size" class="flex flex-col items-center">
<h5 class="mb-2 text-sm font-medium">
size="{{ item.size }}"
</h5>
<NScrollArea
type="always"
:size="item.size"
class="h-52 w-full border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
{{ item.label }}
</h4>
<div v-for="(text, index) in contentItems" :key="index" class="mb-2">
<div class="text-sm">
{{ text }}
</div>
<NSeparator v-if="index !== contentItems.length - 1" class="my-2" />
</div>
</div>
</NScrollArea>
</div>
</div>
</template>
Rounded
Prop | Default | Type | Description |
---|---|---|---|
rounded | full | string | The roundedness of the scroll area. |
Notifications
<script setup lang="ts">
const notifications = [
{ id: 1, title: 'New Feature', message: 'Scroll area component now supports custom rounded corners', time: '10 minutes ago' },
{ id: 2, title: 'Documentation', message: 'Updated examples for better visual representation', time: '45 minutes ago' },
{ id: 3, title: 'Bug Fix', message: 'Fixed overflow issues in mobile view', time: '2 hours ago' },
{ id: 4, title: 'Performance', message: 'Improved scrolling performance on all devices', time: '5 hours ago' },
{ id: 5, title: 'New Feature', message: 'Added multiple color options for scrollbars', time: '1 day ago' },
{ id: 6, title: 'Update', message: 'Core library upgraded to latest version', time: '1 day ago' },
{ id: 7, title: 'Documentation', message: 'Added new examples for customization', time: '2 days ago' },
{ id: 8, title: 'Bug Fix', message: 'Resolved styling conflicts with other components', time: '3 days ago' },
{ id: 9, title: 'Accessibility', message: 'Improved keyboard navigation support', time: '4 days ago' },
{ id: 10, title: 'Performance', message: 'Reduced bundle size by 15%', time: '1 week ago' },
]
</script>
<template>
<div class="grid w-full place-items-center">
<NScrollArea
rounded="0"
class="h-72 w-80 border rounded-md"
>
<div class="p-4">
<h4 class="mb-4 text-sm font-medium leading-none">
Notifications
</h4>
<div v-for="notification in notifications" :key="notification.id">
<div class="text-sm">
<div class="font-medium">
{{ notification.title }}
</div>
<div class="text-xs opacity-80">
{{ notification.message }}
</div>
<div class="mt-1 text-xs opacity-60">
{{ notification.time }}
</div>
</div>
<NSeparator v-if="notification.id !== notifications.length" class="my-2" />
</div>
</div>
</NScrollArea>
</div>
</template>
Customization
You can also use the una
prop to add utility classes, refer to the Props and Presets sections for more information.
Accessible Scrolling
Cross-browser consistent scrollbars that work on all devices with keyboard navigation and screen reader support.
Display Modes
Shows scrollbars when hovering over the scroll area
Always shows scrollbars regardless of interaction
Shows scrollbars during scrolling and hides after
Common Use Cases
Content panels
For dashboards, chat interfaces, and document viewers
Long forms
For registration flows, surveys, and user settings
Code blocks
For documentation, code editors, and technical content
Media galleries
For image collections, video libraries, and portfolios
<template>
<div class="w-full flex justify-center">
<div class="max-w-md w-full">
<NScrollArea
type="hover"
rounded="lg"
class="h-600px border rounded-lg p-4"
:una="{
scrollAreaScrollbar: 'bg-gray-100/50 hover:bg-gray-200/70 dark:bg-gray-800/30 dark:hover:bg-gray-700/50',
scrollAreaThumb: 'bg-primary-500/70 hover:bg-primary-600 dark:bg-primary-400/70 dark:hover:bg-primary-400 rounded-full',
}"
>
<div class="space-y-8">
<!-- Header -->
<div>
<h3 class="text-2xl text-base font-bold">
Accessible Scrolling
</h3>
<p class="mt-2 text-accent">
Cross-browser consistent scrollbars that work on all devices with keyboard navigation and screen reader support.
</p>
</div>
<!-- Display modes -->
<div class="border rounded-md bg-muted p-4">
<h4 class="mb-3 text-sm text-accent font-semibold">
Display Modes
</h4>
<div class="space-y-3">
<div class="flex items-start">
<div class="mr-3 mt-2 h-3 w-3 rounded-full bg-primary" />
<div>
<span class="text-xs text-accent font-medium">hover</span>
<p class="text-xs text-muted">
Shows scrollbars when hovering over the scroll area
</p>
</div>
</div>
<div class="flex items-start">
<div class="mr-3 mt-2 h-3 w-3 rounded-full bg-primary" />
<div>
<span class="text-xs text-accent font-medium">always</span>
<p class="text-xs text-muted">
Always shows scrollbars regardless of interaction
</p>
</div>
</div>
<div class="flex items-start">
<div class="mr-3 mt-2 h-3 w-3 rounded-full bg-primary" />
<div>
<span class="text-xs text-accent font-medium">scroll</span>
<p class="text-xs text-muted">
Shows scrollbars during scrolling and hides after
</p>
</div>
</div>
</div>
</div>
<NSeparator />
<!-- Use cases -->
<div>
<h4 class="mb-3 text-sm text-accent font-semibold">
Common Use Cases
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="border rounded-lg bg-muted p-3">
<div class="mb-2 h-6 w-6 flex items-center justify-center border rounded bg-white">
<div class="h-2 w-2 rounded-full bg-primary" />
</div>
<h5 class="mb-1 text-xs text-accent font-medium">
Content panels
</h5>
<p class="text-xs text-muted">
For dashboards, chat interfaces, and document viewers
</p>
</div>
<div class="border rounded-lg bg-muted p-3">
<div class="mb-2 h-6 w-6 flex items-center justify-center border rounded bg-white">
<div class="h-2 w-2 rounded-full bg-primary" />
</div>
<h5 class="mb-1 text-xs text-accent font-medium">
Long forms
</h5>
<p class="text-xs text-muted">
For registration flows, surveys, and user settings
</p>
</div>
<div class="border rounded-lg bg-muted p-3">
<div class="mb-2 h-6 w-6 flex items-center justify-center border rounded bg-white">
<div class="h-2 w-2 rounded-full bg-primary" />
</div>
<h5 class="mb-1 text-xs text-accent font-medium">
Code blocks
</h5>
<p class="text-xs text-muted">
For documentation, code editors, and technical content
</p>
</div>
<div class="border rounded-lg bg-muted p-3">
<div class="mb-2 h-6 w-6 flex items-center justify-center border rounded bg-white">
<div class="h-2 w-2 rounded-full bg-primary" />
</div>
<h5 class="mb-1 text-xs text-accent font-medium">
Media galleries
</h5>
<p class="text-xs text-muted">
For image collections, video libraries, and portfolios
</p>
</div>
</div>
</div>
</div>
</NScrollArea>
</div>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | The default slot for the ScrollArea component. |
Presets
import type { Theme } from '@unocss/preset-mini'
import type { RuleContext } from 'unocss'
import { parseColor } from '@unocss/preset-mini'
type ScrollAreaPrefix = 'scroll-area'
export const staticScrollArea: Record<`${ScrollAreaPrefix}-${string}` | ScrollAreaPrefix, string> = {
// configurations
'scroll-area': '',
'scroll-area-scrollbar-vertical': 'h-full w-0.625em border-l border-l-transparent p-0.0625em',
'scroll-area-scrollbar-horizontal': 'h-0.625em flex-col border-t border-t-transparent p-0.0625em',
// components
'scroll-area-root': 'relative overflow-hidden',
'scroll-area-scrollbar': 'flex touch-none select-none transition-colors',
'scroll-area-viewport': 'h-full w-full rounded-inherit',
'scroll-area-scrollbar-thumb': 'relative flex-1',
'scroll-area-gray': 'bg-border',
}
export const dynamicScrollArea = [
// dynamic preset
[/^scroll-area(-(\S+))?$/, ([, , c]: [string, string, string], { theme }: RuleContext<Theme>) => {
const parsedColor = parseColor(c, theme)
if ((parsedColor?.cssColor?.type === 'rgb' || parsedColor?.cssColor?.type === 'rgba') && parsedColor.cssColor.components)
return `bg-${c}-200 dark:bg-${c}-700/58`
return undefined
}],
]
export const scrollArea = [
...dynamicScrollArea,
staticScrollArea,
]
Props
import type {
ScrollAreaRootProps,
ScrollAreaScrollbarProps,
ScrollAreaThumbProps,
ScrollAreaViewportProps,
} from 'reka-ui'
import type { HTMLAttributes } from 'vue'
interface BaseExtension {
class?: HTMLAttributes['class']
size?: HTMLAttributes['class']
rounded?: HTMLAttributes['class']
}
export interface NScrollAreaProps extends
ScrollAreaRootProps,
Pick<ScrollAreaScrollbarProps, 'orientation'>,
BaseExtension {
/**
* Allows you to add `UnaUI` scroll area preset properties,
* Think of it as a shortcut for adding options or variants to the preset if available.
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/scroll-area.ts
* @example
* scrollArea="green"
*/
scrollArea?: HTMLAttributes['class']
/**
* The scroll area root props.
*/
_scrollAreaRoot?: NScrollAreaRootProps
/**
* The scroll area scrollbar props.
*/
_scrollAreaScrollbar?: NScrollAreaScrollbarProps
/**
* The scroll area viewport props.
*/
_scrollAreaViewport?: NScrollAreaViewportProps
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/scroll-area.ts
*/
una?: NScrollAreaUnaProps
}
export interface NScrollAreaRootProps extends ScrollAreaRootProps, BaseExtension {
una?: NScrollAreaUnaProps['scrollAreaRoot']
}
export interface NScrollAreaScrollbarProps extends ScrollAreaScrollbarProps, Pick<NScrollAreaProps, 'scrollArea'>, BaseExtension {
una?: Pick<NScrollAreaUnaProps, 'scrollAreaScrollbar' | 'scrollAreaThumb'>
}
export interface NScrollAreaThumbProps extends ScrollAreaThumbProps, BaseExtension {
una?: NScrollAreaUnaProps['scrollAreaThumb']
}
export interface NScrollAreaViewportProps extends ScrollAreaViewportProps, Pick<BaseExtension, 'class'> {
una?: NScrollAreaUnaProps['scrollAreaViewport']
}
export interface NScrollAreaUnaProps {
scrollAreaRoot?: HTMLAttributes['class']
scrollAreaViewport?: HTMLAttributes['class']
scrollAreaScrollbar?: HTMLAttributes['class']
scrollAreaThumb?: HTMLAttributes['class']
}
Components
<script setup lang="ts">
import type { NScrollAreaProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from 'reka-ui'
import { cn } from '../../utils'
import ScrollBar from './ScrollBar.vue'
const props = withDefaults(defineProps<NScrollAreaProps>(), {
scrollArea: 'gray',
rounded: 'full',
size: 'md',
})
const delegatedProps = reactiveOmit(props, ['class', 'size', 'rounded'])
</script>
<template>
<ScrollAreaRoot
v-bind="delegatedProps"
:class="cn(
'scroll-area-root',
props.una?.scrollAreaRoot,
props.class,
)"
>
<ScrollAreaViewport
v-bind="_scrollAreaViewport"
:class="cn(
'scroll-area-viewport',
props.una?.scrollAreaViewport,
_scrollAreaViewport?.class,
)"
>
<slot />
</ScrollAreaViewport>
<ScrollBar
v-bind="_scrollAreaScrollbar"
:orientation="orientation ?? _scrollAreaScrollbar?.orientation"
:una
:size
:rounded
:scroll-area
/>
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
<script setup lang="ts">
import type { NScrollAreaScrollbarProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { ScrollAreaScrollbar, ScrollAreaThumb } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NScrollAreaScrollbarProps>(), {
orientation: 'vertical',
rounded: 'full',
})
const delegatedProps = reactiveOmit(props, ['class', 'size', 'rounded', 'scrollArea'])
const orientationClass = computed(() => {
return props.orientation === 'vertical' ? 'scroll-area-scrollbar-vertical' : 'scroll-area-scrollbar-horizontal'
})
</script>
<template>
<ScrollAreaScrollbar
v-bind="delegatedProps"
:size
:class="
cn(
'scroll-area-scrollbar',
orientationClass,
props.una?.scrollAreaScrollbar,
props.class,
)"
>
<ScrollAreaThumb
:scroll-area
:rounded
:class="cn(
'scroll-area-scrollbar-thumb',
props.una?.scrollAreaThumb,
)"
/>
</ScrollAreaScrollbar>
</template>