Components Scroll area

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Scroll area

One themed treatment for every scrollable region - styled scrollbars, edge fades, and zero JavaScript.
A vertical list

The default. Size the viewport with classes - here max-h-56 - and everything past it scrolls with a thin themed scrollbar instead of the browser's default slab. tabindex="0" comes for free, so the arrow keys and Page Up/Down work the moment you tab into it.

  • Release note 1 v4.1.0
  • Release note 2 v4.2.0
  • Release note 3 v4.3.0
  • Release note 4 v4.4.0
  • Release note 5 v4.5.0
  • Release note 6 v4.6.0
  • Release note 7 v4.7.0
  • Release note 8 v4.8.0
  • Release note 9 v4.9.0
  • Release note 10 v4.10.0
  • Release note 11 v4.11.0
  • Release note 12 v4.12.0
  • Release note 13 v4.13.0
  • Release note 14 v4.14.0
heex
<.scroll_area
aria-label="Recent releases"
class="max-h-56 w-full max-w-sm rounded-lg border border-gray-200 p-3 dark:border-gray-800"
>
<ul class="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li :for={n <- 1..14} class="flex items-center justify-between gap-4">
<span>Release note {n}</span>
<span class="text-xs text-gray-400">v4.{n}.0</span>
</li>
</ul>
</.scroll_area>
Horizontal, with faded edges

A tag rail that runs off the side of a card. fade_edges masks the scrolling axis so the row dissolves at the clip rather than being guillotined - the visual cue that says there is more this way.

Topics
elixir phoenix liveview heex tailwind oban ecto postgres fly accessibility
heex
<div class="w-full max-w-sm rounded-lg border border-gray-200 p-4 dark:border-gray-800">
<div class="mb-3 text-sm font-medium text-gray-900 dark:text-gray-100">Topics</div>
<.scroll_area orientation="horizontal" fade_edges class="w-full pb-2">
<div class="flex w-max gap-2">
<.badge
:for={
tag <- ~w(elixir phoenix liveview heex tailwind oban ecto postgres fly accessibility)
}
label={tag}
variant="soft"
/>
</div>
</.scroll_area>
</div>
Both axes, gutter reserved

A deploy log with orientation="both": one container, two themed scrollbars and a themed corner where they meet. gutter_stable reserves the scrollbar's space up front, so the lines do not shuffle sideways the first time a scrollbar appears. On overlay scrollbars (the macOS default) there is no gutter to reserve and the flag quietly does nothing.

09:14:01.118 [info]  release v4.15.0 building on builder-04 (elixir 1.19.1 / otp 28)
09:14:04.902 [info]  ==> deps compiled in 3.7s, 214 modules
09:14:11.470 [info]  ==> assets: tailwind 4.1.3 wrote priv/static/assets/app.css in 812ms
09:14:12.006 [info]  ==> digest: 46 files, 1.9MB total, gzip 412KB
09:14:19.331 [info]  image pushed: registry.fly.io/acme-prod:deployment-01K2 (sha256:9f21c4ab)
09:14:26.884 [info]  machine 5683d9 updating in syd, waiting for health checks
09:14:38.019 [info]  machine 5683d9 healthy after 11.1s, 1 of 2 machines updated
09:14:51.744 [info]  machine 90e8ff healthy after 9.4s, 2 of 2 machines updated
09:14:52.100 [info]  deployment complete, 0 failed, rollback not required
heex
<.scroll_area
orientation="both"
gutter_stable
aria-label="Deploy log"
class="max-h-48 w-full max-w-md rounded-lg bg-gray-900 p-4 dark:border dark:border-gray-800"
>
<pre class="w-max font-mono text-xs leading-6 text-gray-100">{PetalComponents.Showcase.ScrollArea.deploy_log()}</pre>
</.scroll_area>
Asking for a permanent scrollbar

visibility="always" asks the engine to keep the scrollbar drawn instead of fading it in on scroll. It is a request, not a guarantee: WebKit honours it, Firefox has no mechanism for it, and no browser overrides an OS set to hide scrollbars. Reach for it when a region would otherwise look like static text.

Scrollbars belong to the operating system. This component themes what the platform hands it - the thumb colour, the track, the width where that is ours to set - and leaves the behaviour alone.

That means native momentum scrolling, native keyboard handling and native assistive-tech behaviour, none of which a JavaScript scrollbar reimplementation gets for free.

The trade is that a Mac still looks like a Mac. We think that is the right way round.

heex
<.scroll_area
visibility="always"
class="max-h-40 w-full max-w-sm rounded-lg border border-gray-200 p-3 text-sm text-gray-700 dark:border-gray-800 dark:text-gray-300"
>
<p class="mb-3">
Scrollbars belong to the operating system. This component themes what the platform hands it - the thumb colour, the track, the width where that is ours to set - and leaves the behaviour alone.
</p>
<p class="mb-3">
That means native momentum scrolling, native keyboard handling and native assistive-tech behaviour, none of which a JavaScript scrollbar reimplementation gets for free.
</p>
<p>
The trade is that a Mac still looks like a Mac. We think that is the right way round.
</p>
</.scroll_area>
Properties
Attribute Type Default Description
class any nil size the viewport here, e.g. class="max-h-72" or class="max-w-full" - sizing is deliberately class-driven, not attr-driven
fade_edges boolean false fade content out at the scroll edges with a mask-image gradient, hinting that more content exists past the clip. Masks apply on the scrolling axis only, and are static - they do not track scroll position
gutter_stable boolean false reserve scrollbar space with scrollbar-gutter: stable so content does not shift when the scrollbar appears or disappears. Classic scrollbars only - overlay scrollbars have no gutter to reserve
orientation string "vertical" which axis scrolls: vertical (overflow-y), horizontal (overflow-x), or both
one of: "vertical", "horizontal", "both"
rest global
visibility string "auto" auto follows the platform (overlay scrollbars appear on scroll on macOS); always requests a persistently visible scrollbar where the engine allows it - see the platform-truth note in the module docs
one of: "auto", "always"
:inner_block slot slot