All components alwayshoverxssmmdlg
Scroll Area
Custom scroll containers with a palette-tinted scrollbar in four thicknesses.
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
123456789101112131415161718import { ScrollArea } from "#components/ui/ark-ui/scroll-area.tsx";
export function Example() { return ( // variant/size resolve once on Root; overflow detection, thumb sizing, // and drag scrolling come from the machine — the hover variant reveals // the scrollbar while hovering or scrolling <ScrollArea.Root variant="always" size="md"> <ScrollArea.Viewport> <ScrollArea.Content>{/* long content */}</ScrollArea.Content> </ScrollArea.Viewport> <ScrollArea.Scrollbar orientation="vertical"> <ScrollArea.Thumb /> </ScrollArea.Scrollbar> <ScrollArea.Corner /> </ScrollArea.Root> );}Variants
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Sizes
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Deploy preview ready
Ana joined the team
Billing plan upgraded
New API key created
Domain verified
Webhook delivered
Backup completed
Invite sent to Sam
Install
1npx stylexblocks add scroll-areaSource
components/ui/ark-ui/scroll-area.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178import * as stylex from "@stylexjs/stylex";import { ScrollArea as ArkScrollArea } from "@ark-ui/react/scroll-area";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles } from "#lib/recipe-props.ts";import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'
const scrollAreaRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', position: 'relative', overflow: 'hidden', '--scrollbar-margin': '2px', '--scrollbar-click-area': 'calc(var(--scrollbar-size) + calc(var(--scrollbar-margin) * 2))', },})
const scrollAreaRootSizes = stylex.create({ xs: { '--scrollbar-size': sizes._1, }, sm: { '--scrollbar-size': sizes._1_5, }, md: { '--scrollbar-size': sizes._2, }, lg: { '--scrollbar-size': sizes._3, },})
const scrollAreaViewport = stylex.create({ base: { display: 'flex', flexDirection: 'column', height: '100%', width: '100%', borderRadius: 'inherit', scrollbarWidth: 'none', },})
const scrollAreaContent = stylex.create({ base: { minWidth: '100%', },})
const scrollAreaScrollbar = stylex.create({ base: { display: 'flex', userSelect: 'none', touchAction: 'none', borderRadius: radii.full, transition: 'opacity 150ms 300ms', position: 'relative', margin: 'var(--scrollbar-margin)', backgroundColor: `color-mix(in oklch, ${colorPalette.solid} 10%, transparent)`, '--thumb-bg': `color-mix(in oklch, ${colorPalette.solid} 25%, transparent)`, '::before': { content: '""', position: 'absolute', }, },})
const scrollAreaScrollbarVariants = stylex.create({ hover: { opacity: 0, }, always: { opacity: 1, },})
const scrollAreaThumb = stylex.create({ base: { borderRadius: 'inherit', backgroundColor: 'var(--thumb-bg)', transition: 'backgrounds', },})
const scrollAreaCorner = stylex.create({ base: { backgroundColor: semanticColors.bgMuted, margin: 'var(--scrollbar-margin)', opacity: 0, transition: 'opacity 150ms 300ms', },})
export type ScrollAreaVariant = keyof typeof scrollAreaScrollbarVariants
export type ScrollAreaSize = keyof typeof scrollAreaRootSizes
// The generated recipe colors the scrollbar; its geometry (rail placement// per orientation, thumb fill) lives with the component. The machine// positions the rail and sizes the thumb through inline vars, and stamps// `data-orientation` / `data-hover` / `data-scrolling` for the hover reveal.const chrome = stylex.create({ scrollbar: { flexDirection: { default: null, ':is([data-orientation="vertical"])': "column", }, width: { default: null, ':is([data-orientation="vertical"])': "var(--scrollbar-size)", }, height: { default: null, ':is([data-orientation="horizontal"])': "var(--scrollbar-size)", }, }, scrollbarReveal: { opacity: { default: null, ":is([data-hover], [data-hovering], [data-scrolling])": 1, }, }, thumb: { width: "100%", height: "100%" },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst scrollAreaSlotRecipe = defineSlotRecipe({ slots: { root: { styles: scrollAreaRoot, sizes: scrollAreaRootSizes }, viewport: { styles: scrollAreaViewport }, content: { styles: scrollAreaContent }, scrollbar: { styles: scrollAreaScrollbar, variants: scrollAreaScrollbarVariants, }, thumb: { styles: scrollAreaThumb }, corner: { styles: scrollAreaCorner }, }, defaultVariants: { size: "md", variant: "hover" },});
const { withProvider, withContext } = createSlotRecipeContext(scrollAreaSlotRecipe);
/** * ScrollArea — Ark UI's scroll-area machine wearing the generated recipe. * `<ScrollArea.Root variant size>` resolves the recipe once and every part * inherits it through context; overflow detection, thumb sizing, and drag * scrolling are the machine's. The `hover` variant reveals the scrollbar * while hovering or scrolling. Composition per part via `asChild`. */export const ScrollArea = { Root: withProvider(ArkScrollArea.Root, "root", "ScrollAreaRoot"), Viewport: withContext(ArkScrollArea.Viewport, "viewport", "ScrollAreaViewport"), Content: withContext(ArkScrollArea.Content, "content", "ScrollAreaContent"), Scrollbar: withContext( ArkScrollArea.Scrollbar, (v) => [ ...resolveSlotStyles(scrollAreaSlotRecipe.slots.scrollbar, v), chrome.scrollbar, v.variant === "hover" && chrome.scrollbarReveal, ], "ScrollAreaScrollbar", ), Thumb: withContext( ArkScrollArea.Thumb, () => [scrollAreaThumb.base, chrome.thumb], "ScrollAreaThumb", ), Corner: withContext(ArkScrollArea.Corner, "corner", "ScrollAreaCorner"),};