Scroll Area

A scroll area lets content scroll inside a fixed space, with scrollbars that match the theme.

A scroll area holds content that is larger than the space you give it, such as a long list in a panel or a wide table. It uses the browser's own scrolling and draws thin themed scrollbars on top. For a chat thread that sticks to the newest message, use Message Scroller.

Playground

09:12Ana Rivera merged “Add billing page” into main
09:40Deploy to production finished in 2 minutes
10:05Leo Park opened an issue about slow search results
10:31Nightly backup completed for all three databases
11:02Mia Chen invited two people to the design team
11:48Error rate on the checkout API fell back under 0.1%
12:20Sam Ortiz changed the plan to Team, billed yearly
13:07A new API key was created for the staging app
13:55Preview deploy for branch “fix/cart-total” is ready
14:30Weekly report sent to 14 subscribers

Usage

import { Box, Flex, ScrollArea, Text } from '@kushagradhawan/kookie-ui';
 
export function ActivityPanel({ events }: { events: { id: string; text: string }[] }) {
  return (
    <Box height="320px">
      <ScrollArea scrollbars="vertical" fade>
        <Flex direction="column" gap="3" p="4">
          {events.map((event) => (
            <Text key={event.id} size="2">
              {event.text}
            </Text>
          ))}
        </Flex>
      </ScrollArea>
    </Box>
  );
}

Best practices

Give it a size. A scroll area fills the width and height of its parent. Set the height on a parent Box, or the area grows with its content and never scrolls.

Scroll on one axis. Set scrollbars to the direction the content runs. Content that scrolls both ways is harder to read and to move through.

Don't nest scroll areas on the same axis. A vertical list inside a vertical panel traps the wheel and the trackpad in whichever one is under the pointer.

Let the page scroll when it can. A scroll area suits panels, sidebars and menus with a fixed size. The main content of a page reads better when the page itself scrolls.

Scrollbars

scrollbars picks which bars render: vertical, horizontal or both, the default. With both, a corner fills the space where they meet.

type sets when the bars show. It comes from Radix.

TypeWhen the bars show
hoverWhile the pointer is over the area. This is the default
scrollWhile the content is scrolling
autoWhenever the content overflows
alwaysAll the time, even when nothing overflows

scrollHideDelay is how long, in milliseconds, the bars stay after the pointer leaves or scrolling stops. It is 0 by default, and 600 with type="scroll".

Fade

fade fades the content toward each edge when there is more to scroll past it, on both axes. An edge with nothing beyond it doesn't fade. The fade is a mask on the content, so it works on any background. In a right-to-left layout the side fades follow the reading direction.

Examples

Horizontal row

A row of filters or cards that is wider than its container scrolls sideways, and the page keeps the vertical scroll. Table already wraps itself in a scroll area, so it doesn't need one.

<ScrollArea scrollbars="horizontal">
  <Flex gap="2" pb="3">
    {filters.map((filter) => (
      <Chip key={filter}>{filter}</Chip>
    ))}
  </Flex>
</ScrollArea>

Scrolling with the keyboard

While the content overflows, the viewport is in the tab order, so keyboard users can reach it and scroll with the arrow keys. The area then shows a focus ring. Give it an aria-label, so a screen reader can say what it holds.

<ScrollArea height="240px" aria-label="Terms of service">
  <Text as="p">…</Text>
</ScrollArea>

Props

PropTypeDefaultDescription
scrollbars'vertical' | 'horizontal' | 'both''both'Which scrollbars render
type'hover' | 'scroll' | 'auto' | 'always''hover'When the scrollbars show
scrollHideDelaynumber0, or 600 with type="scroll"Milliseconds before the scrollbars hide
fadebooleanfalseFades each edge when there is more to scroll past it
radius'none' | 'small' | 'medium' | 'large' | 'full'Theme radiusThe corner of the scrollbars
width, minWidth, maxWidthstring, responsive'100%' for widthThe width of the area
height, minHeight, maxHeightstring, responsive'100%' for heightThe height of the area
dir'ltr' | 'rtl''ltr'The reading direction, for the scrollbars and the fade
asChildbooleanfalseRenders the child element as the root

className, style and the size props go on the root. Every other prop, including ref, tabIndex and aria-*, goes on the scrolling viewport, which accepts every attribute of the HTML div element.

Accessibility

KeyAction
Arrow keysScroll when focus is inside the viewport
Page Up or Page DownScroll by a page
Home or EndScroll to the start or the end
  • Native scrolling: The viewport scrolls with the browser's own scrolling, so the keyboard, the wheel, touch and assistive tools all work as they do on any page.
  • Focus: The viewport is in the tab order while it has something to scroll. Inside a menu or a listbox it stays out, because the menu moves focus. Set tabIndex to choose yourself. Focus on items inside it scrolls them into view.
  • Name: Give a scrolling viewport an aria-label. A named viewport is a region, so a screen reader can find it and say what it holds.