Chapter 1
Chapter 2
Chapter 3
Chapter 4
Chapter 5
Chapter 6
Chapter 7
Chapter 8
Chapter 9
Chapter 10
Chapter 11
Chapter 12
Chapter 13
Chapter 14
Chapter 15
Chapter 16
Chapter 17
Chapter 18
Chapter 19
Chapter 20
Chapter 21
Chapter 22
Chapter 23
Chapter 24
<script setup lang="ts">
import { Card, ScrollArea, Stack, Text } from '@hina-ui/vue'
const chapters = Array.from({ length: 24 }, (_, i) => `Chapter ${i + 1}`)
</script>
<template>
<Card class="w-full max-w-xs" :padded="false">
<ScrollArea class="h-64">
<Stack gap="none" class="p-2">
<Text v-for="chapter in chapters" :key="chapter" size="sm" class="px-3 py-2">
{{ chapter }}
</Text>
</Stack>
</ScrollArea>
</Card>
</template>
Usage
import { ScrollArea } from '@hina-ui/vue'
Give the area a height and it scrolls whatever overflows. The scrollbar floats above the content instead of taking width from it, and it looks the same on every platform.
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 11
Line 12
<script setup lang="ts">
import { ScrollArea, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<ScrollArea class="bg-inset h-40 w-full max-w-sm rounded-md">
<Stack class="p-4">
<Text v-for="i in 12" :key="i">Line {{ i }}</Text>
</Stack>
</ScrollArea>
</template>
Every scrolling container in the library goes through this component — dropdowns, menus, the bodies of Dialog and Drawer, the main region of AppShell. Nothing else pulls in a scrolling library of its own, so the behaviour cannot drift apart.
Examples
Direction
direction takes vertical, horizontal or both. In a horizontal area the wheel scrolls sideways, since a mouse has no natural gesture for it and the overlay scrollbar is hidden most of the time.
<script setup lang="ts">
import { Card, Inline, ScrollArea } from '@hina-ui/vue'
</script>
<template>
<ScrollArea direction="horizontal" class="bg-inset w-full max-w-md rounded-md">
<Inline gap="sm" :wrap="false" class="p-4">
<Card
v-for="i in 12"
:key="i"
class="bg-surface grid size-20 shrink-0 place-items-center"
:padded="false"
>
{{ i }}
</Card>
</Inline>
</ScrollArea>
</template>
Content direction is inherited or set explicitly with dir="rtl". Horizontal shadow positions and gradients mirror together, always fading inward from the edge.
Edge shadows
A shadow appears at each edge that has content beyond it, and goes away at the ends. Set shadow to false where the boundary is already obvious.
With edge shadows
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Without them
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
<script setup lang="ts">
import { ScrollArea, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-full max-w-sm">
<Stack gap="xs">
<Text tone="muted" size="sm">With edge shadows</Text>
<ScrollArea class="bg-inset h-32 rounded-md">
<Stack class="p-4">
<Text v-for="i in 10" :key="i">Line {{ i }}</Text>
</Stack>
</ScrollArea>
</Stack>
<Stack gap="xs">
<Text tone="muted" size="sm">Without them</Text>
<ScrollArea :shadow="false" class="bg-inset h-32 rounded-md">
<Stack class="p-4">
<Text v-for="i in 10" :key="i">Line {{ i }}</Text>
</Stack>
</ScrollArea>
</Stack>
</Stack>
</template>
When the scrollbar shows
autoHide decides when the scrollbar fades out: never keeps it, scroll shows it while scrolling, leave keeps it until the pointer leaves, and move shows it on any pointer movement inside.
never
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
scroll
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
leave
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
move
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
<script setup lang="ts">
import { ScrollArea, Stack, Text } from '@hina-ui/vue'
const modes = ['never', 'scroll', 'leave', 'move'] as const
</script>
<template>
<Stack class="w-full max-w-sm">
<Stack v-for="mode in modes" :key="mode" gap="xs">
<Text tone="muted" size="sm">{{ mode }}</Text>
<ScrollArea :auto-hide="mode" class="bg-inset h-24 rounded-md">
<Stack class="p-4">
<Text v-for="i in 8" :key="i">Line {{ i }}</Text>
</Stack>
</ScrollArea>
</Stack>
</Stack>
</template>
Keyboard access
A region that scrolls but holds nothing focusable cannot be reached by keyboard. Set focusable to put it in the tab order as a role="region", and give it a name through label.
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
<script setup lang="ts">
import { ScrollArea, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<ScrollArea focusable label="Release notes" class="bg-inset h-32 w-full max-w-sm rounded-md">
<Stack class="p-4">
<Text v-for="i in 10" :key="i">Line {{ i }}</Text>
</Stack>
</ScrollArea>
</template>
Behaviour
- The scrollbar is laid over the content, so the layout width does not change when content starts to overflow.
- Scrolling works before the component takes over. The custom scrollbar initializes on the next frame after mounting and preserves the native scroll position; opening or reopening an overlay does not wait for idle time or for scrolling to stop.
- Content that changes size through an animation updates the scrollbar on its own; there is no observer to wire up.
- While an overlay that locks the page, such as a dialog or a menu, is open the area stops answering the wheel and touch at once; scroll areas inside the overlay are unaffected, and scrolling resumes when it closes.
Accessibility
- Without
focusablethe area is a plain container: keyboard users reach the focusable elements inside it and the browser scrolls to follow. - With
focusablethe area is arole="region"in the tab order, named bylabel, and the arrow keys scroll it.
API
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which axis scrolls |
dir | 'ltr' | 'rtl' | 'auto' | Inherited | Content direction for the viewport and shadows |
autoHide | 'never' | 'scroll' | 'leave' | 'move' | 'leave' | When the scrollbar fades out |
scrollbar | boolean | true | Whether the scrollbar is visible at all |
wheelRedirect | boolean | true | Wheel scrolls sideways in a horizontal area |
shadow | boolean | true | Whether edges get a shadow |
focusable | boolean | false | Put the area in the tab order |
label | string | — | Accessible name, used when focusable |
class | string | — | Classes appended to the outer element |
| Slot | Description |
|---|---|
default | The scrolled content |
| Exposed | Type | Description |
|---|---|---|
viewport | HTMLElement | undefined | Scroll element, available after initialization on the next frame after mounting. |