EXEPERTAI LAB

Research alpha

Time Machine
EXEPERTAI LAB
GALLERY / COLLECTION
← Browse Astryx gallery
ScrollableArea·component·@astryxdesign/core

Scrollable Area

Provides a native scroll viewport and a real observed content box. The viewport enters the tab order only while a requested logical axis is effectively scrollable, and containment applies only to effective axes.

Open in Playground @astryxdesign/core/ScrollableArea

Showcases and examples

6 documented examples

Scrollable Area

A fixed-height file panel that scrolls on the block axis; the heading sits inside the viewport, so it scrolls away with the content.

Preview loads on approachPreview loads on approach
Exact source · scrollable-area-showcase
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {ScrollableArea} from '@astryxdesign/core/ScrollableArea';
import {Card} from '@astryxdesign/core/Card';
import {Grid} from '@astryxdesign/core/Grid';
import {VStack} from '@astryxdesign/core/Layout';
import {Heading, Text} from '@astryxdesign/core/Text';

const SECTIONS = [
  {
    label: 'Recently opened',
    files: [
      {id: 'brief', title: 'Q3 launch brief', meta: 'Edited 2 days ago'},
      {id: 'pricing', title: 'Pricing rework', meta: 'Edited 3 days ago'},
      {
        id: 'onboarding',
        title: 'Onboarding flow v4',
        meta: 'Edited 4 days ago',
      },
      {id: 'macros', title: 'Support macros', meta: 'Edited last week'},
    ],
  },
  {
    label: 'Shared with you',
    files: [
      {id: 'brand', title: 'Brand refresh', meta: 'Priya Raman'},
      {id: 'checkout', title: 'Checkout audit', meta: 'Tomas Lindqvist'},
      {id: 'partner', title: 'Partner deck', meta: 'Ines Okafor'},
      {id: 'research', title: 'Research synthesis', meta: 'Mei Watanabe'},
    ],
  },
  {
    label: 'Archived',
    files: [
      {id: 'roadmap', title: '2025 roadmap', meta: 'Archived in March'},
      {id: 'legacy', title: 'Legacy pricing', meta: 'Archived in March'},
      {id: 'kit', title: 'Old brand kit', meta: 'Archived in January'},
      {id: 'beta', title: 'Beta feedback', meta: 'Archived in January'},
    ],
  },
];

export default function ScrollableAreaShowcase() {
  return (
    <Card width={380} padding={0}>
      {/* The heading sits inside the viewport, so it scrolls away with the
          content instead of staying pinned above it. */}
      <ScrollableArea
        axis="block"
        role="region"
        label="Workspace files"
        height={300}
        padding={4}>
        <VStack gap={4}>
          <VStack gap={0.5}>
            <Heading level={3}>Workspace files</Heading>
            <Text type="supporting">12 files across 3 sections</Text>
          </VStack>
          {SECTIONS.map(section => (
            <VStack key={section.label} gap={2}>
              <Text type="label" color="secondary">
                {section.label}
              </Text>
              <Grid columns={2} gap={2}>
                {section.files.map(file => (
                  <Card key={file.id} variant="muted" padding={3}>
                    <VStack gap={1}>
                      <Text weight="medium" maxLines={1}>
                        {file.title}
                      </Text>
                      <Text type="supporting" maxLines={1}>
                        {file.meta}
                      </Text>
                    </VStack>
                  </Card>
                ))}
              </Grid>
            </VStack>
          ))}
        </VStack>
      </ScrollableArea>
    </Card>
  );
}

Usage

Provides a native scroll viewport and a real observed content box. The viewport enters the tab order only while a requested logical axis is effectively scrollable, and containment applies only to effective axes.

  • Give every area a concise label that identifies the content keyboard users will scroll.
  • Choose inline, block, or both from content intent; the component maps the logical axes through writing mode and direction.
  • Keep the default overscroll="allow" for nested areas unless the interaction deliberately needs containment.
  • Use useScrollableArea instead when a component already owns both a viewport and a suitable content box, or when children must remain direct flex/grid items or retain a definite percentage block-size basis.
  • ScrollableArea owns one normal block content box with a 100% minimum size. Inline and both-axis modes use max-content inline sizing, so intrinsic inline layout is intentionally wider than the viewport.
  • Use logical padding props on the content box so nested full-bleed components receive the same inset geometry.
  • Set isFullBleed only when the viewport itself should reach an ancestor container edge; it is off by default.
  • Set stickyContainment="always" only when a fitting viewport should intentionally remain a Sticky boundary.
  • Hide the native scrollbar without another visible and operable overflow affordance.
  • Add another overflow wrapper around ScrollableArea; one native viewport should own scrolling.

Typed props

PropType and behavior
axis'inline' | 'block' | 'both' · default 'block'
Logical axis or axes where native scrolling is allowed.
labelstring · required
Accessible name for the viewport when it becomes keyboard scrollable.
role'group' | 'region' · default 'group'
Semantics for the named viewport.
overscroll'allow' | 'contain' · default 'allow'
Whether effective axes continue scrolling an ancestor at their edge.
widthSizeValue
Width of the viewport; a number is interpreted as pixels, a string is used as-is.
heightSizeValue
Height of the viewport; a number is interpreted as pixels, a string is used as-is.
maxWidthSizeValue
Maximum width of the viewport.
minHeightSizeValue
Minimum height of the viewport.
stickyContainment'whenScrollable' | 'always' · default 'whenScrollable'
Whether fitting content passes Sticky ownership to an outer container or deliberately keeps this viewport as the CSS Sticky boundary.
padding0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10 · default 0
Content padding on every logical edge; publishes matching inset geometry.
paddingInline0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical inline-axis content padding; overrides padding on that axis.
paddingInlineStart0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical inline-start content padding; overrides broader padding values.
paddingInlineEnd0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical inline-end content padding; overrides broader padding values.
paddingBlock0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical block-axis content padding; overrides padding on that axis.
paddingBlockStart0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical block-start content padding; overrides broader padding values.
paddingBlockEnd0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10
Logical block-end content padding; overrides broader padding values.
isFullBleedboolean · default false
Lets the viewport escape inherited container padding without changing content padding.
childrenReactNode
Content rendered inside the observed content box.
xstyleStyleXStyles
StyleX sizing and native scrollbar presentation overrides for the viewport.

Anatomy

Viewport · required

The root native scroll container, accessible name owner, focus target while effective, and astryx-scrollable-area theme target.

Content box · required

A real inner layout box observed together with the viewport. For inline scrolling it uses max-content inline sizing with a 100% minimum.

Theming

Targets

astryx-scrollable-area

Visual props: axis