Aksara UI

Scroll Area

Scroll areas scroll their content inside a fixed box and draw a slim scrollbar over it.

import { ScrollArea } from '@aksornproject/aksara-ui'

Vertical scrolling

ScrollArea is a namespace of six parts. Root is the box, and its size sets how much content shows. Viewport fills the Root and scrolls. Content wraps what scrolls. Scrollbar holds a Thumb.

The Scrollbar shows only while the content is taller than the Root. The thumb length is the share of the content in view, so a long list gets a short thumb. The track turns light grey while the pointer is over it or drags the thumb.

<ScrollArea.Root className="h-[240px] w-full max-w-[320px] rounded-radius-radius-8 border border-color-border">
  <ScrollArea.Viewport role="region" aria-label="Lessons">
    <ScrollArea.Content>
      <ul className="flex flex-col gap-spacing-8 p-spacing-16">
        {LESSONS.map((lesson) => (
          <li key={lesson}>{lesson}</li>
        ))}
      </ul>
    </ScrollArea.Content>
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar>
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
</ScrollArea.Root>

Horizontal scrolling

orientation="horizontal" on the Scrollbar draws it along the bottom edge. A wheel over the track scrolls sideways.

<ScrollArea.Root className="w-full max-w-[320px] rounded-radius-radius-8 border border-color-border">
  <ScrollArea.Viewport role="region" aria-label="Units">
    <ScrollArea.Content>
      <ul className="flex w-max gap-spacing-12 p-spacing-16 pb-spacing-20">
        {UNITS.map((unit) => (
          <li
            key={unit}
            className="flex size-[96px] items-center justify-center rounded-radius-radius-8 bg-color-background-muted"
          >
            {unit}
          </li>
        ))}
      </ul>
    </ScrollArea.Content>
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar orientation="horizontal">
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
</ScrollArea.Root>

Both directions

Add a Scrollbar for each direction and a Corner. The Corner fills the square where the two tracks meet, so they never overlap.

<ScrollArea.Root className="h-[240px] w-full max-w-[320px] rounded-radius-radius-8 border border-color-border">
  <ScrollArea.Viewport role="region" aria-label="Timetable">
    <ScrollArea.Content>
      <table>
        <tbody>
          {CLASSES.map((name) => (
            <tr key={name}>
              {PERIODS.map((period) => (
                <td
                  key={period}
                  className="px-spacing-12 py-spacing-4 whitespace-nowrap"
                >
                  {`${name}, ${period}`}
                </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>

Styling

Each part merges className last, so your classes win. Give the Root a height or a width, because the Viewport only scrolls when the Root is smaller than the content. The scrollbars float over the content, so leave room at the edge when text runs under the track.

Accessibility

  • The scrollbars are hidden from assistive technology and never take focus. Keyboard and screen reader users scroll the Viewport.
  • The Viewport becomes a tab stop while its content overflows. Give it role="region" and an aria-label that names the content, such as "Lessons", so a screen reader announces what the focused box holds.
  • After a Tab, the Viewport scrolls with the browser scrolling keys, such as Page Down and End.
  • The focus ring draws around the Viewport while it has keyboard focus.

API

ScrollArea.Root

Prop

Type

ScrollArea.Viewport

Prop

Type

ScrollArea.Content

Prop

Type

ScrollArea.Scrollbar

Prop

Type

ScrollArea.Thumb

Prop

Type

ScrollArea.Corner

Prop

Type

Edit on GitHub

On this page