Scroll Area

Themable scrollbar overlay with consistent appearance across browsers, falling back to native scrolling on touch devices.

Basic

  1. v1.4.0 May 8

    Scroll area component

  2. v1.3.2 May 1

    Calendar bug fixes

  3. v1.3.1 Apr 24

    Tags input refinements

  4. v1.3.0 Apr 18

    Toast notifications

  5. v1.2.4 Apr 9

    Date picker accessibility

  6. v1.2.3 Apr 2

    Sheet swipe gestures

  7. v1.2.2 Mar 27

    Dropdown keyboard nav

  8. v1.2.1 Mar 20

    Form validation polish

  9. v1.2.0 Mar 14

    New form components

  10. v1.1.5 Mar 6

    Performance pass

  11. v1.1.4 Feb 28

    Theme tokens cleanup

  12. v1.1.3 Feb 21

    Modal focus trap

  13. v1.1.2 Feb 15

    Tabs keyboard support

  14. v1.1.1 Feb 8

    Input affordances

.heex
<.scroll_area class="h-80 w-72 rounded-base border border-base" viewport_class="p-4">
  <ol class="space-y-4">
    <li :for={entry <- @changelog} class="flex flex-col gap-1">
      <div class="flex items-center justify-between">
        <span class="font-mono text-xs text-foreground">v{entry.version}</span>
        <span class="text-xs text-foreground-softer">{entry.date}</span>
      </div>
      <p class="text-sm text-foreground">{entry.title}</p>
    </li>
  </ol>
</.scroll_area>

Horizontal scroll

When inner content is wider than the viewport, a horizontal scrollbar appears at the bottom edge.

Dashboard

Admin layout

Auth

Sign in flow

Settings

Account panels

Pricing

Plan comparison

Landing

Marketing home

Docs

Reference site

Empty

Blank starter

.heex
<.scroll_area class="w-full max-w-md rounded-base border border-base">
  <div class="flex gap-3 p-3">
    <article :for={tpl <- @templates} class="w-44 shrink-0 rounded-base border border-base p-3">
      <div class="flex size-9 items-center justify-center rounded-md bg-sunken text-foreground-softer">
        <.icon name={tpl.icon} class="size-5" />
      </div>
      <h4 class="mt-3 text-sm font-medium">{tpl.name}</h4>
      <p class="text-xs text-foreground-softer">{tpl.meta}</p>
    </article>
  </div>
</.scroll_area>

Both axes

When content overflows on both axes, both scrollbars appear and the corner gap is painted so the tracks do not overlap.

.heex
<.scroll_area class="h-64 w-72 rounded-base border border-base" viewport_class="p-3">
  <div class="grid grid-cols-12 gap-1.5 w-[600px]">
    <button
      :for={n <- @cells}
      class="aspect-square rounded-sm border border-base bg-base text-[10px] font-mono text-foreground-softer hover:border-primary/40 hover:bg-primary/5"
    >
      {n}
    </button>
  </div>
</.scroll_area>

Visibility modes

type controls when the custom scrollbar is visible: hover (default) shows it on pointer enter and during scroll, scroll shows it only while scrolling, always keeps it permanently visible, and auto mirrors native behavior.

type="hover"
  1. 01 Foundations
  2. 02 Color tokens
  3. 03 Typography
  4. 04 Spacing
  5. 05 Shadows
  6. 06 Borders
  7. 07 Motion
  8. 08 Icons
  9. 09 Badges
  10. 10 Buttons
  11. 11 Inputs
  12. 12 Selects
  13. 13 Forms
  14. 14 Tables
  15. 15 Modals
  16. 16 Toasts
type="scroll"
  1. 01 Foundations
  2. 02 Color tokens
  3. 03 Typography
  4. 04 Spacing
  5. 05 Shadows
  6. 06 Borders
  7. 07 Motion
  8. 08 Icons
  9. 09 Badges
  10. 10 Buttons
  11. 11 Inputs
  12. 12 Selects
  13. 13 Forms
  14. 14 Tables
  15. 15 Modals
  16. 16 Toasts
type="always"
  1. 01 Foundations
  2. 02 Color tokens
  3. 03 Typography
  4. 04 Spacing
  5. 05 Shadows
  6. 06 Borders
  7. 07 Motion
  8. 08 Icons
  9. 09 Badges
  10. 10 Buttons
  11. 11 Inputs
  12. 12 Selects
  13. 13 Forms
  14. 14 Tables
  15. 15 Modals
  16. 16 Toasts
type="auto"
  1. 01 Foundations
  2. 02 Color tokens
  3. 03 Typography
  4. 04 Spacing
  5. 05 Shadows
  6. 06 Borders
  7. 07 Motion
  8. 08 Icons
  9. 09 Badges
  10. 10 Buttons
  11. 11 Inputs
  12. 12 Selects
  13. 13 Forms
  14. 14 Tables
  15. 15 Modals
  16. 16 Toasts
.heex
<.scroll_area type="hover" class="h-48">...</.scroll_area>
<.scroll_area type="scroll" class="h-48">...</.scroll_area>
<.scroll_area type="always" class="h-48">...</.scroll_area>
<.scroll_area type="auto" class="h-48">...</.scroll_area>

Scroll hide delay

scroll_hide_delay sets how long the scrollbar lingers after the last scroll event (milliseconds). Only meaningful for hover and scroll. Scroll the two panels and notice how long the bar stays after each flick.

scroll_hide_delay={300} Snappy
  1. 01 Line of content
  2. 02 Line of content
  3. 03 Line of content
  4. 04 Line of content
  5. 05 Line of content
  6. 06 Line of content
  7. 07 Line of content
  8. 08 Line of content
  9. 09 Line of content
  10. 10 Line of content
  11. 11 Line of content
  12. 12 Line of content
  13. 13 Line of content
  14. 14 Line of content
  15. 15 Line of content
  16. 16 Line of content
  17. 17 Line of content
  18. 18 Line of content
  19. 19 Line of content
  20. 20 Line of content
  21. 21 Line of content
  22. 22 Line of content
  23. 23 Line of content
  24. 24 Line of content
  25. 25 Line of content
  26. 26 Line of content
  27. 27 Line of content
  28. 28 Line of content
  29. 29 Line of content
  30. 30 Line of content
scroll_hide_delay={2000} Lingering
  1. 01 Line of content
  2. 02 Line of content
  3. 03 Line of content
  4. 04 Line of content
  5. 05 Line of content
  6. 06 Line of content
  7. 07 Line of content
  8. 08 Line of content
  9. 09 Line of content
  10. 10 Line of content
  11. 11 Line of content
  12. 12 Line of content
  13. 13 Line of content
  14. 14 Line of content
  15. 15 Line of content
  16. 16 Line of content
  17. 17 Line of content
  18. 18 Line of content
  19. 19 Line of content
  20. 20 Line of content
  21. 21 Line of content
  22. 22 Line of content
  23. 23 Line of content
  24. 24 Line of content
  25. 25 Line of content
  26. 26 Line of content
  27. 27 Line of content
  28. 28 Line of content
  29. 29 Line of content
  30. 30 Line of content
.heex
<.scroll_area type="scroll" scroll_hide_delay={300}>...</.scroll_area>
<.scroll_area type="scroll" scroll_hide_delay={2000}>...</.scroll_area>

Custom scrollbar styling

scrollbar_class resizes or recolors the track; thumb_class targets the thumb. The thumb's along-scroll size is computed at runtime from the content ratio and cannot be set through classes.

Default
  • 01 Item
  • 02 Item
  • 03 Item
  • 04 Item
  • 05 Item
  • 06 Item
  • 07 Item
  • 08 Item
  • 09 Item
  • 10 Item
  • 11 Item
  • 12 Item
  • 13 Item
  • 14 Item
  • 15 Item
  • 16 Item
  • 17 Item
  • 18 Item
  • 19 Item
  • 20 Item
  • 21 Item
  • 22 Item
  • 23 Item
  • 24 Item
  • 25 Item
  • 26 Item
  • 27 Item
  • 28 Item
Branded
  • 01 Item
  • 02 Item
  • 03 Item
  • 04 Item
  • 05 Item
  • 06 Item
  • 07 Item
  • 08 Item
  • 09 Item
  • 10 Item
  • 11 Item
  • 12 Item
  • 13 Item
  • 14 Item
  • 15 Item
  • 16 Item
  • 17 Item
  • 18 Item
  • 19 Item
  • 20 Item
  • 21 Item
  • 22 Item
  • 23 Item
  • 24 Item
  • 25 Item
  • 26 Item
  • 27 Item
  • 28 Item
.heex
<.scroll_area
  class="h-80 rounded-base border border-base"
  viewport_class="p-4"
  scrollbar_class="data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:p-0.5"
  thumb_class="bg-primary rounded-full"
  type="always"
>
  ...
</.scroll_area>

Edge fade

The root carries data-overflow-y-start and data-overflow-y-end while content is hidden behind the top or bottom edge. Pair them with ::before / ::after gradients for a clean mask.

Release 1.4.0

Scroll area, refined

This release introduces the new scroll_area component, a themable overlay that hides the native scrollbar on fine pointer devices and falls back to system scrolling on touch.

What's new

  • Vertical, horizontal, and dual axis scrolling.
  • Four visibility modes that match different surfaces.
  • Data attributes for edge fades and scroll-aware UI.
  • Class hooks on the track and thumb for full theming.

Notes

The component preserves native momentum, rubber banding, and keyboard scrolling. On coarse pointer devices it stays out of the way and lets the OS render the bar.

Pair it with the new theme tokens to align the thumb color with your brand, or hide the scrollbar entirely while keeping the scroll affordances intact.

Breaking changes

None. The component is additive and can be dropped into any existing layout without affecting other surfaces.

.heex
<.scroll_area
  class="h-80 rounded-base border border-base
         before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-10
         before:bg-gradient-to-b before:from-base before:to-transparent
         before:opacity-0 data-[overflow-y-start]:before:opacity-100 before:transition-opacity
         after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-10
         after:bg-gradient-to-t after:from-base after:to-transparent
         after:opacity-0 data-[overflow-y-end]:after:opacity-100 after:transition-opacity"
  viewport_class="p-5"
>
  ...
</.scroll_area>

Wrap a navlist in a flex-fill scroll area so the workspace header and footer stay pinned while the items scroll.

.heex
<aside class="flex h-screen w-64 flex-col border-r border-base">
  <div class="border-b border-base p-3">...workspace header...</div>

  <.scroll_area class="flex-1" viewport_class="px-4 py-3">
    <.navlist heading="Workspace">
      <.navlink :for={item <- @primary} navigate={item.path} active={item.active}>
        <.icon name={item.icon} class="size-5" /> {item.label}
      </.navlink>
    </.navlist>

    <.navlist heading="Projects">
      <.navlink :for={p <- @projects} navigate={p.path}>
        <.icon name="hero-folder" class="size-5" /> {p.name}
      </.navlink>
    </.navlist>
  </.scroll_area>

  <div class="border-t border-base p-3">...account footer...</div>
</aside>

Constrain a modal body with scroll_area so a long form, terms text, or settings panel scrolls inside the dialog without pushing the chrome offscreen.

.heex
<.button phx-click={Fluxon.open_dialog("terms")}>Open terms</.button>

<.modal id="terms" class="w-full max-w-md p-0">
  <div class="border-b border-base px-5 py-4">
    <h3 class="text-base font-semibold">Terms of service</h3>
    <p class="mt-0.5 text-xs text-foreground-softer">Last updated May 8, 2026</p>
  </div>

  <.scroll_area class="h-[60vh]" viewport_class="px-5 py-5 space-y-7" type="auto">
    <section :for={section <- @sections} class="space-y-2">
      <h4 class="text-sm font-semibold">{section.title}</h4>
      <p class="text-sm text-foreground-soft leading-relaxed">{section.body}</p>
    </section>
  </.scroll_area>

  <div class="flex justify-end gap-2 border-t border-base px-5 py-3">
    <.button variant="ghost" phx-click={Fluxon.close_dialog("terms")}>Decline</.button>
    <.button phx-click={Fluxon.close_dialog("terms")}>I agree</.button>
  </div>
</.modal>

Card carousel

Horizontal scroll inside a card section. Pair with type="scroll" so the bar appears only while flicking through.

Recently viewed

SR

Site refresh

Edited 2h ago

MO

Mobile onboarding

Edited yesterday

PA

Public API

Edited 3d ago

BG

Brand guidelines

Edited last week

SI

Search index

Edited 2 weeks ago

BV

Billing v2

Edited last month

IT

Internal tools

Archived

MP

Migration plan

Draft

.heex
<section class="rounded-base border border-base">
  <div class="flex items-center justify-between border-b border-base px-3 py-2">
    <h3 class="text-sm font-medium">Recently viewed</h3>
    <div class="flex items-center gap-1">
      <.button size="icon-xs" variant="ghost" aria-label="Previous">
        <.icon name="hero-arrow-left" class="icon" />
      </.button>
      <.button size="icon-xs" variant="ghost" aria-label="Next">
        <.icon name="hero-arrow-right" class="icon" />
      </.button>
    </div>
  </div>

  <.scroll_area type="scroll" scroll_hide_delay={400}>
    <div class="flex gap-3 p-3">
      <article :for={item <- @items} class="w-44 shrink-0 rounded-base border border-base p-3">
        <div class={["mb-2 aspect-video rounded-sm", item.thumb]}></div>
        <h4 class="truncate text-sm font-medium">{item.title}</h4>
        <p class="mt-0.5 text-xs text-foreground-softer">{item.meta}</p>
      </article>
    </div>
  </.scroll_area>
</section>

Wide table

Wrap a wide data table to keep the layout from blowing out the page while preserving the column widths a dense table needs.

Project Owner Region Plan Status Last deploy
fluxon-core Ada Lovelace us-east-1 Pro Live 2 hours ago
fluxland-web Grace Hopper eu-west-1 Team Live 5 minutes ago
fluxon-cli Linus Torvalds ap-south-1 Free Building Yesterday
fluxon-docs Margaret Hamilton us-west-2 Pro Live 10 minutes ago
fluxon-icons Barbara Liskov eu-central-1 Team Failed 3 hours ago
.heex
<.scroll_area class="rounded-base border border-base" viewport_class="px-3">
  <.table>
    <.table_head>
      <:col>Project</:col>
      <:col>Owner</:col>
      <:col>Region</:col>
      <:col>Plan</:col>
      <:col>Status</:col>
      <:col>Last deploy</:col>
    </.table_head>
    <.table_body>
      <.table_row :for={row <- @rows}>
        <:cell>{row.project}</:cell>
        <:cell>{row.owner}</:cell>
        <:cell>{row.region}</:cell>
        <:cell>{row.plan}</:cell>
        <:cell>
          <.badge color={row.status_color} variant="soft" size="xs">{row.status}</.badge>
        </:cell>
        <:cell>{row.last_deploy}</:cell>
      </.table_row>
    </.table_body>
  </.table>
</.scroll_area>