Skip to content
cennge-ui
GitHubv0.3.1, changelog

ScrollArea

A box that scrolls on its own, with thin scrollbars in the theme's colours, optional fades at the edges that hide more, and a tab stop once it overflows.

  1. 881e770test(next): look from both sides of the server's time zone on every run
  2. 4baadf3fix(playground): read the repositories' update dates in UTC
  3. 20668ecdocs(changeset): release the README with the New York maps
  4. 28a4731test(map): set the example maps in New York
  5. 055d5effix(kbd): draw the key symbols at the size of the cap
  6. 5252be2docs(changeset): describe the dialog backdrop as it ships
  7. 18bec4bdocs: the theme switch, charts, and what changed in dialogs
  8. 3bacd80test: skip the focus ring check for a fixture with nothing to focus
  9. 29dd316test(theme): run on React 17 and Testing Library 12 too
  10. f614e25fix(dialog): draw the backdrop beside the dialog, not around it
  11. 9b1a5a4feat(charts): Sparkline
  12. cdb8e4efeat(charts): DonutChart
  13. 5696189feat(charts): LineChart and AreaChart
  14. db78740feat(charts): BarChart, in a new entry cennge-ui/charts
  15. 65666cftest(playground): more time for the full-page axe audits
  16. 32b28a0feat(theme): ThemeScript, useTheme and ThemeSwitch
  17. 76281e6test(select): a tap on an option does not press what is under the list
  18. e067d76fix(select): the chevron turns only while its own list is open
  19. 63baf9efix(dialog): popups in a portal scroll inside a modal dialog
  20. d948c07fix(button): the pointer cursor, and the wait cursor while loading
cennge-uiai-niche-scoutpayload-cachespredo-crypto-screenerplaywright-msi-scraper
  1. Hover the commits: a thin bar in the theme's colours fades in, and its thumb drags.
  2. Tab reaches each box because it scrolls; the arrow keys, Page Up, Page Down, Home and End scroll it.
  3. Long messages end in an ellipsis instead of widening the box; the fades show which edges hide more.
Live: hover, drag, tab in and use the keys.

Field marks

Import
import { ScrollArea } from "cennge-ui";
Size
1.7 KB gzip for this import, its dependencies aside
Pattern
Native scrolling under drawn scrollbars; a named region once it overflows
Keyboard
Arrow up Arrow down Arrow left Arrow right PageUpPageDownHomeEnd
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Touch
No hover on touch screens, so the bars show while the finger scrolls
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: scroll-area

When to use#

Use a ScrollArea for a part of the page that scrolls on its own: a commit list in a panel, a log, a row of filters. It keeps the browser's own scrolling, with its momentum, wheel and touch, and draws the scrollbars in the theme's colours on every system.

Similar#

  • VirtualList renders only the rows in view; use it for thousands of rows. A ScrollArea renders all of its content.
  • Carousel snaps slides into place as they scroll sideways, with controls to step through them.

A list with long lines#

Vertical content keeps the width of the box, so truncate ends a long line with an ellipsis instead of making the box scroll sideways. fadeEdges fades the content out at each edge that still hides some of it; fadeSize sets how far the fade reaches.

  1. 881e770test(next): look from both sides of the server's time zone on every run
  2. 4baadf3fix(playground): read the repositories' update dates in UTC
  3. 28a4731test(map): set the example maps in New York
  4. 055d5effix(kbd): draw the key symbols at the size of the cap
  5. f614e25fix(dialog): draw the backdrop beside the dialog, not around it
  6. 9b1a5a4feat(charts): Sparkline
  7. cdb8e4efeat(charts): DonutChart
  8. 5696189feat(charts): LineChart and AreaChart
  9. db78740feat(charts): BarChart, in a new entry cennge-ui/charts
  10. 32b28a0feat(theme): ThemeScript, useTheme and ThemeSwitch
commits.tsx
import { ScrollArea } from "cennge-ui";

const commits = [
  ["881e770", "test(next): look from both sides of the server's time zone on every run"],
  ["4baadf3", "fix(playground): read the repositories' update dates in UTC"],
  ["28a4731", "test(map): set the example maps in New York"],
  ["055d5ef", "fix(kbd): draw the key symbols at the size of the cap"],
  ["f614e25", "fix(dialog): draw the backdrop beside the dialog, not around it"],
  ["9b1a5a4", "feat(charts): Sparkline"],
  ["cdb8e4e", "feat(charts): DonutChart"],
  ["5696189", "feat(charts): LineChart and AreaChart"],
  ["db78740", "feat(charts): BarChart, in a new entry cennge-ui/charts"],
  ["32b28a0", "feat(theme): ThemeScript, useTheme and ThemeSwitch"],
] as const;

export default function Commits() {
  return (
    <ScrollArea aria-label="Commits" fadeEdges className="h-56 w-full max-w-md rounded-large border border-border bg-surface-1">
      <ol className="flex flex-col p-2">
        {commits.map(([sha, message]) => (
          <li key={sha} className="flex items-baseline gap-3 px-2 py-1.5 text-sm">
            <code className="shrink-0 text-xs text-muted">{sha}</code>
            <span className="truncate">{message}</span>
          </li>
        ))}
      </ol>
    </ScrollArea>
  );
}

Both ways#

orientation="horizontal" scrolls a row sideways, as the repositories on the plate do; "both" lets wide and tall content scroll either way, and a corner joins the two bars. type says when the bars show: on hover (the default), while scrolling, whenever the content overflows (auto), or always.

881e770d9d4fb09e61eae8ce6069b29b654aa65f  2026-11-01  test(next): look from both sides of the server's time zone on every run
4baadf3249a748daa07dbdedeb1f05da99e91eee  2026-11-01  fix(playground): read the repositories' update dates in UTC
c2d6d1bf9c74ae87b8fbcf76431c832756c3d3fb  2026-10-31  chore: release
20668ec40bb6cad661b45a49c1b610174fc0a207  2026-10-31  docs(changeset): release the README with the New York maps
28a47314cc0a7a3d4bb833b58c60af625be23a26  2026-10-31  test(map): set the example maps in New York
5b54e0607e0462cd8a45f372a8ec85f535d22ae9  2026-10-31  chore: release
055d5ef8c1f6617d6d81eb04e8816f79b509cfde  2026-10-31  fix(kbd): draw the key symbols at the size of the cap
29dd316ec890cb1d7532b19148a99d4f94a95569  2026-10-31  test(theme): run on React 17 and Testing Library 12 too
f614e2513f4b8ec77d3ef7cba02188ce0c0f9c96  2026-10-31  fix(dialog): draw the backdrop beside the dialog, not around it
both.tsx
import { ScrollArea } from "cennge-ui";

const log = `881e770d9d4fb09e61eae8ce6069b29b654aa65f  2026-11-01  test(next): look from both sides of the server's time zone on every run
4baadf3249a748daa07dbdedeb1f05da99e91eee  2026-11-01  fix(playground): read the repositories' update dates in UTC
c2d6d1bf9c74ae87b8fbcf76431c832756c3d3fb  2026-10-31  chore: release
20668ec40bb6cad661b45a49c1b610174fc0a207  2026-10-31  docs(changeset): release the README with the New York maps
28a47314cc0a7a3d4bb833b58c60af625be23a26  2026-10-31  test(map): set the example maps in New York
5b54e0607e0462cd8a45f372a8ec85f535d22ae9  2026-10-31  chore: release
055d5ef8c1f6617d6d81eb04e8816f79b509cfde  2026-10-31  fix(kbd): draw the key symbols at the size of the cap
29dd316ec890cb1d7532b19148a99d4f94a95569  2026-10-31  test(theme): run on React 17 and Testing Library 12 too
f614e2513f4b8ec77d3ef7cba02188ce0c0f9c96  2026-10-31  fix(dialog): draw the backdrop beside the dialog, not around it`;

export default function GitLog() {
  return (
    <ScrollArea aria-label="git log" orientation="both" type="auto" className="h-48 w-full rounded-large border border-border bg-surface-1">
      <pre className="p-4 font-mono text-xs leading-6">{log}</pre>
    </ScrollArea>
  );
}

Scroll from code#

viewportRef hands you the element that scrolls, for scrollTo, for measuring, or for a virtual list of your own.

  1. cennge-ui 0.3.1
  2. cennge-ui 0.3.0
  3. cennge-ui 0.2.0
  4. cennge-ui 0.1.1
  5. cennge-ui 0.1.0
from-code.tsx
import { useRef } from "react";
import { Button, ScrollArea } from "cennge-ui";

const releases = [
  { version: "0.3.1", date: "2026-11-01" },
  { version: "0.3.0", date: "2026-10-31" },
  { version: "0.2.0", date: "2026-10-21" },
  { version: "0.1.1", date: "2026-10-20" },
  { version: "0.1.0", date: "2026-10-19" },
];

export default function JumpToEnd() {
  const viewport = useRef<HTMLDivElement>(null);

  const scrollTo = (top: number) => viewport.current?.scrollTo({ top, behavior: "smooth" });

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <ScrollArea aria-label="Releases" viewportRef={viewport} type="always" className="h-40 rounded-large border border-border bg-surface-1">
        <ol className="flex flex-col p-2">
          {releases.map((release) => (
            <li key={release.version} className="flex items-baseline justify-between gap-3 rounded-small px-3 py-2.5">
              <span className="text-sm font-medium">cennge-ui {release.version}</span>
              <time dateTime={release.date} className="text-xs text-muted tabular-nums">
                {release.date}
              </time>
            </li>
          ))}
        </ol>
      </ScrollArea>
      <div className="flex gap-2">
        <Button size="sm" variant="flat" onClick={() => scrollTo(0)}>
          Newest
        </Button>
        <Button size="sm" variant="flat" onClick={() => scrollTo(viewport.current?.scrollHeight ?? 0)}>
          Oldest
        </Button>
      </div>
    </div>
  );
}

Accessibility#

A box whose content fits is not a tab stop and has no role. Once the content overflows it takes focus from Tab, so keyboard users can scroll it, and with an aria-label it becomes a region that screen readers list. Name every ScrollArea that may overflow.

Styling#

classNames reaches the parts: base (the box), viewport (the element that scrolls), scrollbar and thumb. Give the box its height, border and background with className.

Props#

PropTypeDefault
orientation
"horizontal""vertical""both"Which way the content scrolls. Vertical content keeps the width of the area, so long lines wrap and truncate ends them with "…"; horizontal and both let content grow sideways.
"vertical"
type
"auto""hover""scroll""always"When the scrollbars show: on hover, while scrolling, whenever the content overflows, or always. On touch screens, which have no hover, "hover" shows them while scrolling.
"hover"
scrollHideDelay
numberHow long scrollbars stay after the pointer leaves or scrolling stops, in ms.
600
fadeEdges
booleanFade the content out at edges that hide more of it, so it is clear there is more to scroll.
–
fadeSize
stringLength of the fade.
"1.5rem"
viewportRef
React.Ref<HTMLDivElement>The scrolling element, e.g. to scroll from code or to measure for a virtual list.
–
classNames
Partial<Record<"base" | "viewport" | "scrollbar" | "thumb", string>>
–

Also takes every attribute of <div>, except dir, which mean something else here.