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:
CustomScrollonly 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… |
|---|---|
|
|
Basic example — long text
Multi-page content inside a fixed height. Scroll in the area below to see the custom bar appear on hover.
import { CustomScroll } from '@apollion-dsi/core/containers/scroll';
<CustomScroll height={240}>{/* long content */}</CustomScroll>;Example with fixed title
<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.
<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.
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