Scroll Area
A scroll container with propel's overlay scrollbar that reveals on hover or scroll, built on Base UI ScrollArea.
Basic
Astra updated the project roadmap — item 1
Astra updated the project roadmap — item 2
Astra updated the project roadmap — item 3
Astra updated the project roadmap — item 4
Astra updated the project roadmap — item 5
Astra updated the project roadmap — item 6
Astra updated the project roadmap — item 7
Astra updated the project roadmap — item 8
Astra updated the project roadmap — item 9
Astra updated the project roadmap — item 10
Astra updated the project roadmap — item 11
Astra updated the project roadmap — item 12
Astra updated the project roadmap — item 13
Astra updated the project roadmap — item 14
Astra updated the project roadmap — item 15
Astra updated the project roadmap — item 16
Astra updated the project roadmap — item 17
Astra updated the project roadmap — item 18
Astra updated the project roadmap — item 19
Astra updated the project roadmap — item 20
Astra updated the project roadmap — item 21
Astra updated the project roadmap — item 22
Astra updated the project roadmap — item 23
Astra updated the project roadmap — item 24
Show codeHide code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
const ACTIVITY = Array.from(
{ length: 24 },
(_, i) => `Astra updated the project roadmap — item ${i + 1}`,
);
export default function BasicDemo() {
// ScrollArea fills its parent, so it needs a height-constrained flex column to
// bound and scroll within. Inline styles keep the demo self-contained.
return (
<div style={{ display: "flex", flexDirection: "column", height: "16rem", width: "18rem" }}>
<ScrollArea orientation="vertical" visibility="auto" magnitude="thin">
<div
style={{ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.75rem" }}
>
{ACTIVITY.map((line) => (
<p key={line}>{line}</p>
))}
</div>
</ScrollArea>
</div>
);
}Horizontal
Show codeHide code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
const MEMBERS = Array.from({ length: 16 }, (_, i) => `Teammate ${i + 1}`);
export default function HorizontalDemo() {
// Horizontal overflow only: a single horizontal scrollbar, shown on demand. The
// content is a `w-max` row so it overflows sideways within the fixed-width parent.
return (
<div style={{ display: "flex", flexDirection: "column", height: "9rem", width: "18rem" }}>
<ScrollArea orientation="horizontal" visibility="auto" magnitude="thin">
<div style={{ display: "flex", width: "max-content", gap: "0.75rem", padding: "0.75rem" }}>
{MEMBERS.map((member) => (
<div
key={member}
style={{
display: "flex",
height: "4rem",
width: "9rem",
flexShrink: 0,
alignItems: "center",
justifyContent: "center",
borderRadius: "0.375rem",
background: "var(--color-layer-2)",
}}
>
{member}
</div>
))}
</div>
</ScrollArea>
</div>
);
}Both axes
Line 1 of wide content that overflows horizontally as well as down.
Line 2 of wide content that overflows horizontally as well as down.
Line 3 of wide content that overflows horizontally as well as down.
Line 4 of wide content that overflows horizontally as well as down.
Line 5 of wide content that overflows horizontally as well as down.
Line 6 of wide content that overflows horizontally as well as down.
Line 7 of wide content that overflows horizontally as well as down.
Line 8 of wide content that overflows horizontally as well as down.
Line 9 of wide content that overflows horizontally as well as down.
Line 10 of wide content that overflows horizontally as well as down.
Line 11 of wide content that overflows horizontally as well as down.
Line 12 of wide content that overflows horizontally as well as down.
Line 13 of wide content that overflows horizontally as well as down.
Line 14 of wide content that overflows horizontally as well as down.
Line 15 of wide content that overflows horizontally as well as down.
Line 16 of wide content that overflows horizontally as well as down.
Line 17 of wide content that overflows horizontally as well as down.
Line 18 of wide content that overflows horizontally as well as down.
Line 19 of wide content that overflows horizontally as well as down.
Line 20 of wide content that overflows horizontally as well as down.
Line 21 of wide content that overflows horizontally as well as down.
Line 22 of wide content that overflows horizontally as well as down.
Line 23 of wide content that overflows horizontally as well as down.
Line 24 of wide content that overflows horizontally as well as down.
Line 25 of wide content that overflows horizontally as well as down.
Line 26 of wide content that overflows horizontally as well as down.
Line 27 of wide content that overflows horizontally as well as down.
Line 28 of wide content that overflows horizontally as well as down.
Line 29 of wide content that overflows horizontally as well as down.
Line 30 of wide content that overflows horizontally as well as down.
Show codeHide code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
const LINES = Array.from(
{ length: 30 },
(_, i) => `Line ${i + 1} of wide content that overflows horizontally as well as down.`,
);
export default function BothAxesDemo() {
// Both axes overflow: a vertical and a horizontal scrollbar, each shown on demand,
// plus the corner where they meet. The content is wider and taller than the parent.
return (
<div style={{ display: "flex", flexDirection: "column", height: "16rem", width: "18rem" }}>
<ScrollArea orientation="both" visibility="auto" magnitude="thin">
<div
style={{
display: "flex",
width: "40rem",
flexDirection: "column",
gap: "0.5rem",
padding: "0.75rem",
}}
>
{LINES.map((line) => (
<p key={line} style={{ whiteSpace: "nowrap" }}>
{line}
</p>
))}
</div>
</ScrollArea>
</div>
);
}Always visible
Line 1 of the scrollable content.
Line 2 of the scrollable content.
Line 3 of the scrollable content.
Line 4 of the scrollable content.
Line 5 of the scrollable content.
Line 6 of the scrollable content.
Line 7 of the scrollable content.
Line 8 of the scrollable content.
Line 9 of the scrollable content.
Line 10 of the scrollable content.
Line 11 of the scrollable content.
Line 12 of the scrollable content.
Line 13 of the scrollable content.
Line 14 of the scrollable content.
Line 15 of the scrollable content.
Line 16 of the scrollable content.
Line 17 of the scrollable content.
Line 18 of the scrollable content.
Line 19 of the scrollable content.
Line 20 of the scrollable content.
Line 21 of the scrollable content.
Line 22 of the scrollable content.
Line 23 of the scrollable content.
Line 24 of the scrollable content.
Line 25 of the scrollable content.
Line 26 of the scrollable content.
Line 27 of the scrollable content.
Line 28 of the scrollable content.
Line 29 of the scrollable content.
Line 30 of the scrollable content.
Show codeHide code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
const LINES = Array.from({ length: 30 }, (_, i) => `Line ${i + 1} of the scrollable content.`);
export default function AlwaysVisibleDemo() {
// `visibility="always"` keeps the scrollbar permanently visible instead of revealing
// it on hover/scroll — useful for embedded editors or data tables where users expect
// a persistent rail. Paired here with the roomier `standard` gutter.
return (
<div style={{ display: "flex", flexDirection: "column", height: "16rem", width: "18rem" }}>
<ScrollArea orientation="vertical" visibility="always" magnitude="standard">
<div
style={{ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.75rem" }}
>
{LINES.map((line) => (
<p key={line}>{line}</p>
))}
</div>
</ScrollArea>
</div>
);
}Magnitudes
The magnitude prop sets the scrollbar gutter: thin (12 px) for dense UI, standard (16 px) for roomier panels.
thin
Line 1 of the scrollable content.
Line 2 of the scrollable content.
Line 3 of the scrollable content.
Line 4 of the scrollable content.
Line 5 of the scrollable content.
Line 6 of the scrollable content.
Line 7 of the scrollable content.
Line 8 of the scrollable content.
Line 9 of the scrollable content.
Line 10 of the scrollable content.
Line 11 of the scrollable content.
Line 12 of the scrollable content.
Line 13 of the scrollable content.
Line 14 of the scrollable content.
Line 15 of the scrollable content.
Line 16 of the scrollable content.
Line 17 of the scrollable content.
Line 18 of the scrollable content.
Line 19 of the scrollable content.
Line 20 of the scrollable content.
Line 21 of the scrollable content.
Line 22 of the scrollable content.
Line 23 of the scrollable content.
Line 24 of the scrollable content.
Line 25 of the scrollable content.
Line 26 of the scrollable content.
Line 27 of the scrollable content.
Line 28 of the scrollable content.
Line 29 of the scrollable content.
Line 30 of the scrollable content.
standard
Line 1 of the scrollable content.
Line 2 of the scrollable content.
Line 3 of the scrollable content.
Line 4 of the scrollable content.
Line 5 of the scrollable content.
Line 6 of the scrollable content.
Line 7 of the scrollable content.
Line 8 of the scrollable content.
Line 9 of the scrollable content.
Line 10 of the scrollable content.
Line 11 of the scrollable content.
Line 12 of the scrollable content.
Line 13 of the scrollable content.
Line 14 of the scrollable content.
Line 15 of the scrollable content.
Line 16 of the scrollable content.
Line 17 of the scrollable content.
Line 18 of the scrollable content.
Line 19 of the scrollable content.
Line 20 of the scrollable content.
Line 21 of the scrollable content.
Line 22 of the scrollable content.
Line 23 of the scrollable content.
Line 24 of the scrollable content.
Line 25 of the scrollable content.
Line 26 of the scrollable content.
Line 27 of the scrollable content.
Line 28 of the scrollable content.
Line 29 of the scrollable content.
Line 30 of the scrollable content.
Show codeHide code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
const MAGNITUDES = ["thin", "standard"] as const;
const LINES = Array.from({ length: 30 }, (_, i) => `Line ${i + 1} of the scrollable content.`);
export default function MagnitudesDemo() {
// Both scrollbar gutter steps side by side: `thin` — 12 px gutter, for dense UI like
// menus and pickers; `standard` — 16 px gutter, for roomier panels. Shown with
// `visibility="always"` so both rails are visible at rest.
return (
<div style={{ display: "flex", gap: "0.75rem" }}>
{MAGNITUDES.map((magnitude) => (
<div key={magnitude} style={{ display: "flex", flexDirection: "column", gap: "0.375rem" }}>
<p>{magnitude}</p>
<div
style={{ display: "flex", flexDirection: "column", height: "14rem", width: "12rem" }}
>
<ScrollArea orientation="vertical" visibility="always" magnitude={magnitude}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
padding: "0.75rem",
}}
>
{LINES.map((line) => (
<p key={line}>{line}</p>
))}
</div>
</ScrollArea>
</div>
</div>
))}
</div>
);
}Installation
import { ScrollArea } from "@makeplane/propel/components/scroll-area";
Props
ScrollArea
| Prop | Type | Required | Description |
|---|---|---|---|
| orientation | "vertical" | "horizontal" | "both" | Yes | Which axes scroll (required, no silent default). `vertical`/`horizontal` render a single scrollbar; `both` renders both plus the corner. Render only the axes the content can actually overflow so an unused scrollbar never reserves space or reveals. |
| visibility | "auto" | "always" | Yes | When the scrollbar is shown. `auto` hides it at rest and reveals it on hover/scroll (fades when idle). `always` keeps it permanently visible. Required — pick the behavior that matches the context (e.g. `auto` for panels, `always` for persistent scroll indicators). |
| magnitude | "thin" | "standard" | Yes | Scrollbar gutter size. `thin` — 12 px (3 px padding, 6 px thumb). `standard` — 16 px (5 px padding, 6 px thumb). Required — choose based on the density of the surrounding UI. |
| children | ReactNode | Yes | The scrollable content. |