ScrollArea

A scrolling region with overlay scrollbars.

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>
vue

Usage

import { ScrollArea } from '@hina-ui/vue'
ts

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>
vue

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.

1
2
3
4
5
6
7
8
9
10
11
12
<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>
vue

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>
vue

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>
vue

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>
vue

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 focusable the area is a plain container: keyboard users reach the focusable elements inside it and the browser scrolls to follow.
  • With focusable the area is a role="region" in the tab order, named by label, 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.