Propel

Scroll Area

A scroll container with propel's overlay scrollbar that reveals on hover or scroll, built on Base UI ScrollArea.

Show 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">
        <div
          style={{ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.75rem" }}
        >
          {ACTIVITY.map((line) => (
            <p key={line}>{line}</p>
          ))}
        </div>
      </ScrollArea>
    </div>
  );
}

Installation

import { ScrollArea } from "@makeplane/propel/components/scroll-area";

Usage

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">
        <div
          style={{ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.75rem" }}
        >
          {ACTIVITY.map((line) => (
            <p key={line}>{line}</p>
          ))}
        </div>
      </ScrollArea>
    </div>
  );
}

Children render inside the viewport. Base UI’s optional ScrollArea.Content wrapper is not part of the ready-made — add it yourself only if you need that slot.

Examples

Horizontal

Show 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">
        <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

Show 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">
        <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

Show 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 `lg` gutter.
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "16rem", width: "18rem" }}>
      <ScrollArea orientation="vertical" visibility="always" size="lg">
        <div
          style={{ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.75rem" }}
        >
          {LINES.map((line) => (
            <p key={line}>{line}</p>
          ))}
        </div>
      </ScrollArea>
    </div>
  );
}

Sizes

The size prop sets the scrollbar gutter (same pad as native scrollbar-*): sm (12 px / 4 → 4 px thumb), md (14 / 4 → 6), lg (16 / 4 → 8).

sm

md

lg

Show code
import { ScrollArea } from "@makeplane/propel/components/scroll-area";

const SIZES = ["sm", "md", "lg"] as const;

const LINES = Array.from({ length: 30 }, (_, i) => `Line ${i + 1} of the scrollable content.`);

export default function SizesDemo() {
  // All three scrollbar gutter steps: `sm` — 12 / 4 → 4 px thumb; `md` — 14 / 4 → 6;
  // `lg` — 16 / 4 → 8. Shown with `visibility="always"` so rails are visible at rest.
  return (
    <div style={{ display: "flex", gap: "0.75rem" }}>
      {SIZES.map((size) => (
        <div key={size} style={{ display: "flex", flexDirection: "column", gap: "0.375rem" }}>
          <p>{size}</p>
          <div
            style={{ display: "flex", flexDirection: "column", height: "14rem", width: "12rem" }}
          >
            <ScrollArea orientation="vertical" visibility="always" size={size}>
              <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>
  );
}

Native scrollbars

Propel also ships opt-in utilities that re-skin the browser’s own scrollbar: scrollbar-sm, scrollbar-md, and scrollbar-lg (from @makeplane/propel/styles). Put the class on any overflow element — no ScrollArea required.

Utility Track Inset Visible thumb
scrollbar-sm 12px 4px 4px
scrollbar-md 14px 4px 6px
scrollbar-lg 16px 4px 8px

Same size ladder as ScrollArea’s size prop. Thumb colors use --scrollbar-thumb* (hidden at rest, stronger on container hover / thumb hover / drag).

Use native utilities when you want light styling on a normal overflow: auto region (text areas, dense panels) and can accept browser limits (Firefox: thinner control; no custom inset / per-thumb states).

Use ScrollArea when you need a consistent overlay thumb across browsers (including Firefox), controlled show/hide (visibility), or both-axis rails + corner.

<div className="scrollbar-sm max-h-96 overflow-y-auto">{/* … */}</div>

API Reference

ScrollArea

A scroll container with propel's overlay scrollbar, built on Base UI `ScrollArea`. Use it to wrap any overflowing content (menus, panels, long lists). Place it as a child of a height-constrained flex column: it grows to fill the column and its viewport scrolls when the content overflows.

PropTypeDefaultDescription
orientation(required)"both" | "horizontal" | "vertical"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.
children(required)ReactNodeThe scrollable content (rendered inside the viewport).
visibility"auto" | "always"autoWhen the scrollbar is shown. `auto` hides it at rest and reveals it on hover/scroll (fades when idle). `always` keeps it permanently visible.
size"sm" | "md" | "lg"smScrollbar gutter size (Figma Scrollbar set). Stable full gutter; pad matches native `scrollbar-*`: `sm` — 12 px / 4 → 4 px thumb; `md` — 14 / 4 → 6; `lg` — 16 / 4 → 8.