<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const events = [
["08:42", "Mesh uploaded", "geometry.step"],
["08:44", "Surface checks complete", "128 patches"],
["08:47", "Volume mesh started", "16 workers"],
["08:51", "Boundary layers complete", "12 layers"],
["08:54", "Quality report ready", "0 critical cells"],
];
</script>
<template>
<ScrollArea.Root class="activity-log">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Recent activity">
<ScrollArea.Content>
<ol>
<li v-for="event in events" :key="event[0]">
<time>{{ event[0] }}</time>
<span>{{ event[1] }} — {{ event[2] }}</span>
</li>
</ol>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.activity-log {
block-size: 16rem;
}
</style>Installation
Scroll Area is part of the main Kappa package. Ark UI measures overflow, maps pointer and touch input, keeps native scrolling, and reports edge state.
Barrel
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from "@dicehub/kappa";Granular
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from "@dicehub/kappa/components/scroll-area";Usage
Give Root a fixed block size. Put the full content in Viewport and Content, then add one Scrollbar for each overflow axis. The vertical orientation is Kappa's convenience default.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const events = [
["08:42", "Mesh uploaded", "geometry.step"],
["08:44", "Surface checks complete", "128 patches"],
["08:47", "Volume mesh started", "16 workers"],
["08:51", "Boundary layers complete", "12 layers"],
["08:54", "Quality report ready", "0 critical cells"],
];
</script>
<template>
<ScrollArea.Root class="activity-log">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Recent activity">
<ScrollArea.Content>
<ol>
<li v-for="event in events" :key="event[0]">
<time>{{ event[0] }}</time>
<span>{{ event[1] }} — {{ event[2] }}</span>
</li>
</ol>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.activity-log {
block-size: 16rem;
}
</style>Composition
ScrollArea.Root
├── ScrollArea.Viewport
│ └── ScrollArea.Content
├── ScrollArea.Scrollbar [vertical]
│ └── ScrollArea.Thumb
├── ScrollArea.Scrollbar [horizontal] (optional)
│ └── ScrollArea.Thumb
├── ScrollArea.Corner (with both axes)
└── ScrollArea.Context (optional)Ark UI owns native scrolling, overflow detection, drag behavior, and scroll progress. Kappa owns the quiet tracks, visible thumbs, focus treatment, logical placement, and compound exports.
The examples cover vertical, horizontal, and right-to-left scrolling. The two-axis, nested, and context examples expose the wider Ark UI contract.
Viewportremains the native scroll container.Contentgives Ark UI one element to measure.- Each
Scrollbarneeds oneThumb. - Add
Corneronly when both axes can overflow.
Examples
Horizontal
Add one horizontal Scrollbar when content grows along the inline axis.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const cases = ["Baseline", "Low speed", "Cruise", "High speed", "Crosswind"];
</script>
<template>
<ScrollArea.Root class="case-strip">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Cases">
<ScrollArea.Content class="case-strip__content">
<article v-for="name in cases" :key="name">{{ name }}</article>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="horizontal">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.case-strip { block-size: 11rem; }
.case-strip__content { display: flex; inline-size: max-content; }
.case-strip__content article { flex: none; inline-size: 10.5rem; }
</style>Both directions
Add both scrollbars and Corner for wide, tall data such as a result matrix.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const rows = [
["Baseline", "1,842,000", "420", "1.8e-5", "28 min", "Complete"],
["Low speed", "2,016,000", "458", "2.5e-5", "35 min", "Complete"],
["Cruise", "2,190,000", "496", "3.2e-5", "42 min", "Complete"],
["High speed", "2,364,000", "534", "3.9e-5", "49 min", "Complete"],
["Crosswind", "2,538,000", "572", "4.6e-5", "56 min", "Complete"],
];
</script>
<template>
<ScrollArea.Root class="result-matrix">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Case comparison">
<ScrollArea.Content>
<table>
<thead>
<tr>
<th>Case</th><th>Cells</th><th>Iterations</th>
<th>Residual</th><th>Runtime</th><th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row[0]">
<td v-for="value in row" :key="value">{{ value }}</td>
</tr>
</tbody>
</table>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar orientation="horizontal">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
</template>
<style>
.result-matrix { block-size: 15rem; }
.result-matrix table { inline-size: 48rem; }
</style>Nested
Each nested area owns its viewport and axis. Keep nested scrolling uncommon and deliberate.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const files = ["mesh.case", "solver.log", "forces.csv", "report.pdf", "archive.zip"];
</script>
<template>
<ScrollArea.Root class="notes">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Project notes">
<ScrollArea.Content>
<h3>Project notes</h3>
<p v-for="index in 6" :key="index">
Iteration {{ index }} confirms stable convergence across the operating range.
</p>
<ScrollArea.Root class="files">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Referenced files">
<ScrollArea.Content class="files__content">
<code v-for="file in files" :key="file">{{ file }}</code>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="horizontal">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.notes { block-size: 18rem; }
.files { block-size: 5rem; }
.files__content { display: flex; inline-size: max-content; }
.files__content code { inline-size: 8rem; }
</style>Programmatic controls
Use Context for edge state and focused controls that move the viewport.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const entries = Array.from({ length: 24 }, (_, index) => "Log entry " + (index + 1));
</script>
<template>
<ScrollArea.Root class="event-log">
<ScrollArea.Context v-slot="api">
<button type="button" @click="api.scrollToEdge({ edge: 'top', behavior: 'smooth' })">
First
</button>
<button type="button" @click="api.scrollToEdge({ edge: 'bottom', behavior: 'smooth' })">
Last
</button>
</ScrollArea.Context>
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Controlled event log">
<ScrollArea.Content>
<ol>
<li v-for="entry in entries" :key="entry">{{ entry }}</li>
</ol>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.event-log { block-size: 18rem; }
</style>Without overflow
Ark UI hides the custom scrollbar when the content already fits.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
</script>
<template>
<ScrollArea.Root class="short-content">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Short content">
<ScrollArea.Content>
<p>No scrollbar is shown when the content fits.</p>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar>
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>Right to left
Logical positioning keeps the horizontal control correct in a right-to-left root.
<script setup>
import { ScrollArea } from "@dicehub/kappa/components/scroll-area";
const cases = ["الأساس", "سرعة منخفضة", "الإبحار", "سرعة عالية", "رياح جانبية"];
</script>
<template>
<ScrollArea.Root dir="rtl" class="case-strip">
<ScrollArea.Viewport tabindex="0" role="region" aria-label="Cases, right to left">
<ScrollArea.Content class="case-strip__content">
<article v-for="name in cases" :key="name">{{ name }}</article>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="horizontal">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</template>
<style>
.case-strip { block-size: 11rem; }
.case-strip__content { display: flex; inline-size: max-content; }
.case-strip__content article { flex: none; inline-size: 10.5rem; }
</style>Accessibility
- Keep native wheel, touch, trackpad, and keyboard scrolling available.
- Add
tabindex=0when keyboard users need to enter a non-interactive viewport. - Give a focusable viewport a concise accessible name.
- Do not hide essential actions outside the initial viewport without a visible overflow cue.
- The thumb has a larger invisible hit target for precise pointer and touch input.
- Forced colors keep the track and thumb visible. Reduced motion removes transitions.
API Reference
ScrollArea.Root
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | generated | Stable identifier for the scroll-area machine. |
dir | "ltr" | "rtl" | inherited | Overrides the inherited Ark UI locale direction. |
ids | Partial<ScrollAreaElementIds> | generated | Overrides identifiers for the root, viewport, content, scrollbars, and thumbs. |
asChild | boolean | false | Merges root behavior into the direct child. |
ScrollArea.Scrollbar
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Selects the axis controlled by this scrollbar. |
asChild | boolean | false | Merges scrollbar behavior into the direct child. |
Parts
| Part | Element | Description |
|---|---|---|
ScrollArea.Viewport | div | Native scrolling viewport. Give it a keyboard focus target and accessible name when needed. |
ScrollArea.Content | div | Container measured by Ark UI to detect overflow. |
ScrollArea.Scrollbar | div | Pointer and touch track for one axis. |
ScrollArea.Thumb | div | Draggable control that reports the visible content proportion. |
ScrollArea.Corner | div | Fills the intersection when both scrollbars are present. |
ScrollArea.Context | renderless | Exposes overflow state, edge state, and programmatic scroll methods. |
Exports
| Export | Description |
|---|---|
ScrollArea | Styled compound component with all parts. |
ScrollAreaRoot / ScrollAreaRootProvider | Direct root and external-machine provider components. |
ScrollAreaViewport / ScrollAreaContent | Granular viewport and measured content parts. |
ScrollAreaScrollbar / ScrollAreaThumb / ScrollAreaCorner | Granular custom scrollbar parts. |
ScrollAreaContext | Renderless access to the active Ark UI context. |
useScrollArea / useScrollAreaContext | Ark UI composition functions. |
ScrollArea*Props / ScrollArea*Slots / ScrollAreaApi | Public TypeScript contracts. |