Skip to content
Kappa
Scroll Area
@dicehub/kappav0.4.2

Scroll Area

Adds precise, visible scroll controls to content that exceeds a fixed viewport.

<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

Typical part hierarchy
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.

  • Viewport remains the native scroll container.
  • Content gives Ark UI one element to measure.
  • Each Scrollbar needs one Thumb.
  • Add Corner only 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=0 when 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

PropTypeDefaultDescription
idstringgeneratedStable identifier for the scroll-area machine.
dir"ltr" | "rtl"inheritedOverrides the inherited Ark UI locale direction.
idsPartial<ScrollAreaElementIds>generatedOverrides identifiers for the root, viewport, content, scrollbars, and thumbs.
asChildbooleanfalseMerges root behavior into the direct child.

ScrollArea.Scrollbar

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Selects the axis controlled by this scrollbar.
asChildbooleanfalseMerges scrollbar behavior into the direct child.

Parts

PartElementDescription
ScrollArea.ViewportdivNative scrolling viewport. Give it a keyboard focus target and accessible name when needed.
ScrollArea.ContentdivContainer measured by Ark UI to detect overflow.
ScrollArea.ScrollbardivPointer and touch track for one axis.
ScrollArea.ThumbdivDraggable control that reports the visible content proportion.
ScrollArea.CornerdivFills the intersection when both scrollbars are present.
ScrollArea.ContextrenderlessExposes overflow state, edge state, and programmatic scroll methods.

Exports

ExportDescription
ScrollAreaStyled compound component with all parts.
ScrollAreaRoot / ScrollAreaRootProviderDirect root and external-machine provider components.
ScrollAreaViewport / ScrollAreaContentGranular viewport and measured content parts.
ScrollAreaScrollbar / ScrollAreaThumb / ScrollAreaCornerGranular custom scrollbar parts.
ScrollAreaContextRenderless access to the active Ark UI context.
useScrollArea / useScrollAreaContextArk UI composition functions.
ScrollArea*Props / ScrollArea*Slots / ScrollAreaApiPublic TypeScript contracts.