Components · Data display
Scroll area
BetaA scrollable region with styled scrollbars.
- Source
components/ui/scroll-area.tsx - shadcn/ui Scroll area
- Figma: not linked yet (Q13)
Exercise library
When to use
- For a region with a fixed height inside a page, whose content is longer: the exercise library list next to the program builder, a message thread, a list of meals in a sheet.
- For a horizontal strip that would otherwise break the layout: the weeks of a program, progress photos.
- When the scrollbar should look the same in every browser and both themes.
When not to use
- For the page itself. Let the page scroll with the browser's own scrollbar.
- For short content. Fixed heights with scrolling inside hide content; let the content set the height when it can.
- For tables: Table has its own scrolling container.
- Inside a Dialog or sheet body, which already scrolls. Nested scrolling regions are hard to use with a trackpad and a keyboard.
Anatomy
- 1. Viewport: the visible region; content scrolls inside it
- 2. Scrollbar and thumb, at the end edge; horizontal ones sit at the bottom
When both directions scroll, a corner fills the space where the two scrollbars meet.
Variants
| Variant | Use for |
|---|---|
| Vertical (default) | Lists and threads with a fixed height. |
Horizontal (add <ScrollBar orientation="horizontal" />) | A row of cards or columns, such as program weeks. Give the content w-max so it doesn't wrap. |
When the scrollbar shows
Radix's type prop decides when the scrollbar is visible. shadcn's default is "hover", which hides it until the pointer is over the area, so nothing tells the coach that there is more to see.
type | Scrollbar | Use |
|---|---|---|
"auto" | Visible whenever the content overflows | The default for this system. Used in every example on this page. |
"always" | Always visible, even when nothing overflows | Lists whose length changes, so the layout doesn't shift. |
"scroll" | Only while scrolling | Avoid. |
"hover" (shadcn default) | Only while the pointer is over the area | Avoid: invisible on touch screens and to keyboard users. |
States
| State | What changes | Token |
|---|---|---|
| Content overflows | Thumb shows, sized to the visible share | bg-border, rounded-full |
| Scrolling | Thumb moves; the track stays transparent | — |
| Focus-visible (viewport) | Ring | ring-ring/50 |
The thumb is bg-border, which is a very light line on the background. A proposal for a stronger thumb is listed under Accessibility.
Usage guidelines
- Size the area so a partial item shows at the edge. A cut-off row is the clearest sign that the list scrolls.
- Never hide the scrollbar completely. Use
type="auto". - Keep a heading or label outside the area, so it stays visible while the content scrolls.
- For long message threads, start scrolled to the newest message.
Content guidelines
- Name what scrolls with a visible heading: "Exercise library", "Messages with Sara Al-Harbi".
- In a thread, mark who wrote each message for screen readers ("You:", "Sara:") even when the layout shows it.
Accessibility
| Key | Does |
|---|---|
| Tab | Moves to the next focusable item inside the area, scrolling it into view |
| Arrow up / down | Scroll the area, once focus is inside it |
| Page up / Page down, Space | Scroll by one screen |
| Home / End | Scroll to the top / bottom |
- When its content overflows, the viewport becomes focusable, so keyboard users can always scroll it, even when nothing inside can take focus (a message thread, the program weeks). It shows the focus ring.
- Give the area an
aria-label: it is then announced as a named region ("Messages with Sara Al-Harbi, region"). - Upstream shadcn's viewport is never focusable; this version adds it (axe
scrollable-region-focusable).
- Give the area an
- The scrollbar is visual only; screen readers ignore it, and scroll as they read.
- Never trap focus in an area: Tab always continues past the end.
- The thumb is
bg-border, 1.2:1 on the background in both themes, below the 3:1 that WCAG 1.4.11 asks of a control you can see and drag.muted-foregroundwould be 6.0:1. This is a token choice, so it is raised as a question rather than changed.
RTL
The vertical scrollbar moves to the left edge, the end in RTL, because the site provides Radix's DirectionProvider. Horizontal areas start at the right and scroll toward the left; the thumb starts at the right. Content inside follows the reading order. The scrollbar's 1px spacer border is set with border-l; it's transparent, so it doesn't show in either direction.
Tokens used
Read from components/ui/scroll-area.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --border | bg-border | neutral-200 | neutral-200 |
| --ring | ring-ring/50 | brand-600 | orange-500 |
- Radius
rounded-full
Props
<ScrollArea>
Also takes every prop of Radix ScrollArea.Root
No props of its own.
<ScrollBar>
Also takes every prop of Radix ScrollArea.ScrollAreaScrollbar
No props of its own.
type, scrollHideDelay and dir come from Radix ScrollArea.
Replaces in coach-admin
From the read-only audit of coach-admin: 46 style values, 84 uses.
coach-admin styles scrollbars one container at a time with ::-webkit-scrollbar rules (exercise library, drawers, the matrix slider, schedule lists), each with its own width and colors, and Firefox's scrollbar-color in a few places. ScrollArea replaces them with one style.
Classes and selectors
&::-webkit-scrollbar-thumb29&::-webkit-scrollbar17&::-webkit-scrollbar-track15.videos-scroll-container5&::-webkit-scrollbar-thumb:hover3.exercise-library__content::-webkit-scrollbar-thumb, .exerci2.scroll-list-container2.drawer__content::-webkit-scrollbar-thumb1.matrix-slider__content::-webkit-scrollbar-thumb1.exercise-library__content::-webkit-scrollbar-thumb:hover, .1.matrix-slider__content::-webkit-scrollbar-thumb:hover1.drawer__content::-webkit-scrollbar-thumb:hover1
SCSS variables in these styles
$sidebar-bg→ nearest neutral-800 (no semantic token)$aside-menu-bgnot a color: spacing, size or layout$scroll-thumb-color→ nearest neutral-300 (no semantic token)$disable-color→ nearest neutral-400 (no semantic token)$ai-primary-color→ nearest violet-ai-500 (no semantic token)