Docs
Scroll

CustomScroll

CustomScroll replaces the default scrollbar with a thinner version harmonized with the design system. The native scrollbar is kept active (for accessibility and gestures) but visually hidden; a custom track is drawn over the content and becomes visible on hover.

On macOS in "Automatically based on mouse" mode (where the system hides the native scrollbar), the custom track is also hidden — the user falls back to the behavior they already expect in that environment.

Important: CustomScroll only supports vertical scrolling. For horizontal scrolling, use the browser's native scroll.

height is required in practice — without it the container has no bounded size, so there is no overflow and nothing to scroll.

When to use

✅ Use when…🚫 Avoid when…
  • In areas with long content inside a fixed layout (modals, drawers, cards, side panels) — when the OS native scrollbar would be too thick.
  • To standardize the bar's appearance across operating systems that draw very different scrollbars (Windows vs macOS vs Linux).
  • In very large lists/grids where virtualization would be more appropriate (use libs like react-window or react-virtual).
  • For horizontal scrolling — not supported.
  • On entire pages: let the body's native scroll handle it.

Basic example — long text

Multi-page content inside a fixed height. Scroll in the area below to see the custom bar appear on hover.

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Sed cum quisquam sapiente ab consequatur natus ducimus obcaecati minus rerum adipisci laudantium debitis, perferendis pariatur iste provident dolorum ratione voluptate, repudiandae placeat inventore amet quasi ea facere? Delectus numquam vel inventore quasi excepturi. Minus nulla dolore ad animi pariatur provident assumenda dolorum voluptatem porro harum quo aspernatur error placeat sapiente, magnam, atque id molestiae dignissimos corrupti nobis doloremque iure.

Voluptate nulla itaque vitae provident, voluptatem numquam sequi suscipit ducimus similique quidem, neque minima architecto tempore asperiores, nihil quae ratione dicta quia unde consectetur tenetur! Aspernatur eveniet dolorem nesciunt sint et eligendi enim quidem blanditiis tenetur! Quasi voluptatum repellat unde quod nesciunt commodi, repudiandae beatae illum praesentium veritatis vel quos asperiores. Reiciendis, accusantium cum eveniet dolore quaerat aliquam labore dolor obcaecati non doloribus modi atque?

Excepturi ea quo iste! Sapiente quaerat, dolorem omnis quasi maxime quia accusamus vitae sint at recusandae provident. Quisquam quo voluptates a expedita dolorum ipsam ratione ab molestias dignissimos accusantium adipisci eius, animi tempora atque laborum hic. Dolorum, distinctio? Voluptatum, dolorum repellat odio rerum repudiandae, consequuntur quia tempora vel commodi sapiente debitis officia nesciunt aspernatur deserunt adipisci accusantium quos beatae ratione quasi enim, sequi nulla.

Repellendus iure necessitatibus delectus possimus exercitationem rem itaque ex, dignissimos cupiditate consequuntur perspiciatis nesciunt, dolore voluptas modi expedita facilis. Quisquam, quos. Optio nesciunt eligendi quod amet eius doloribus officia repellat aliquid voluptatem itaque. Repellat consectetur sapiente veniam tempore voluptates dolore nisi, ex assumenda magni perferendis sit, ad dignissimos placeat dolorem aliquid quia, nemo error voluptatibus quasi.

import { CustomScroll } from '@apollion-dsi/core/containers/scroll';
 
<CustomScroll height={240}>{/* long content */}</CustomScroll>;

Example with fixed title

Terms of Use and Privacy Policy

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Sed cum quisquam sapiente ab consequatur natus ducimus obcaecati minus rerum adipisci laudantium debitis, perferendis pariatur iste provident dolorum ratione voluptate, repudiandae placeat inventore amet quasi ea facere?

<CustomScroll height={200}>
  <Flex gap="large">
    <Text fontWeight="bold" fontSize="large" align="center" display="block">
      Terms of Use and Privacy Policy
    </Text>
    <Text>Lorem ipsum dolor sit amet…</Text>
  </Flex>
</CustomScroll>

Example — long list (50 items)

When the content is a list of rows, the custom scroll keeps the same behavior. Hover to see the bar; click + drag to drag it.

1. List item — sample content.2. List item — sample content.3. List item — sample content.4. List item — sample content.5. List item — sample content.6. List item — sample content.7. List item — sample content.8. List item — sample content.9. List item — sample content.10. List item — sample content.11. List item — sample content.12. List item — sample content.13. List item — sample content.14. List item — sample content.15. List item — sample content.16. List item — sample content.17. List item — sample content.18. List item — sample content.19. List item — sample content.20. List item — sample content.21. List item — sample content.22. List item — sample content.23. List item — sample content.24. List item — sample content.25. List item — sample content.26. List item — sample content.27. List item — sample content.28. List item — sample content.29. List item — sample content.30. List item — sample content.31. List item — sample content.32. List item — sample content.33. List item — sample content.34. List item — sample content.35. List item — sample content.36. List item — sample content.37. List item — sample content.38. List item — sample content.39. List item — sample content.40. List item — sample content.41. List item — sample content.42. List item — sample content.43. List item — sample content.44. List item — sample content.45. List item — sample content.46. List item — sample content.47. List item — sample content.48. List item — sample content.49. List item — sample content.50. List item — sample content.
<CustomScroll height={200}>
  <Flex gap="micro">
    {items.map((item) => (
      <Text key={item.id}>{item.label}</Text>
    ))}
  </Flex>
</CustomScroll>

Example — code block

Useful for showing logs, stack traces or long code snippets inside modals or side panels.

$ yarn build[copyStorybook] copied storybook -> public/storybook ▲ Next.js 15.5.18 - Environments: .env.productionCreating an optimized production build ...Compiled successfully in 14sSkipping lintingChecking validity of types ...Collecting page data ...Generating static pages (0/63) ...Generating static pages (15/63)Generating static pages (31/63)Generating static pages (47/63)Generating static pages (63/63)Finalizing page optimization ...Collecting build traces ...✓ Compiled successfully

Properties

Prop
Type
Default
Description
color
string
Theme token (`theme.colors`) used as `color`. Has lower precedence than `contrast`: when both are provided, the contrast calculation wins. Stays `string` (not the typed union): `color` is also an HTML attribute (`<li color>`, etc.) and interfaces extending native props require identical types on the name collision.
containment
string
Containment marker for the `cq` channel: emits `container-type` (+ `container-name` when the string form carries one, e.g. `'inline-size card'`). Apply on the immediate wrapper of the adapting component — NEVER on page-level shells.
legibility
"on-photo"
Reading-shadow preset for text over a photographic background (`on-photo`). Replaces the inline `style={{ textShadow }}` in the consumer (brasil_2030 radar, gap A3). Token emission is deferred until a 2nd consumer asks for the raw var (see backlog).
pageShell
boolean
Centers the element and caps the width at `theme.layout.pageMaxWidth`. The page shell of a classic centered layout.
readable
number | boolean
Makes the `color` legible against the page surface: `true` = WCAG AA (4.5), a number sets a custom floor. Ignored with `contrast`. See the Layout Props concept page for the full semantics.
scrollableRef
Ref<HTMLDivElement>
Ref to the inner node that actually scrolls — what virtualizers need as their scroll element (the outer `ref` is the clipped container).
transform
string
CSS `transform` value (e.g. `translate(-50%, -33px)`). Never leaks to the DOM as an attribute.

Besides the props above, every component accepts the layout props (spacing, color, flex/grid, size, border) — not repeated here.

See also

  • Storybook story: Components / CustomScroll