Propel

Scroll Area

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

Basic

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

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

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 `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

standard

Show 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

PropTypeRequiredDescription
orientation"vertical" | "horizontal" | "both"YesWhich 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"YesWhen 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"YesScrollbar 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.
childrenReactNodeYesThe scrollable content.