All components alwayshoverxssmmdlg
Scroll Area
Custom scroll containers with a palette-tinted scrollbar in four thicknesses.
123456789101112131415161718import { ScrollArea } from "#components/ui/base-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
Sizes
Install
1npx stylexblocks add scroll-areaSource
components/ui/base-ui/scroll-area.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211import * as stylex from "@stylexjs/stylex";import { ScrollArea as BaseScrollArea } from "@base-ui-components/react/scroll-area";import { createSlotRecipeContext } from "#lib/base-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 from Base UI's sizing vars) lives with the// component. The engine stamps `data-orientation` / `data-hovering` /// `data-scrolling` for the hover reveal.const chrome = stylex.create({ scrollbar: { position: "absolute", 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)", }, top: { default: null, ':is([data-orientation="vertical"])': "var(--scrollbar-margin)", }, bottom: { default: null, ':is([data-orientation="vertical"])': "var(--scrollbar-margin)", ':is([data-orientation="horizontal"])': 0, }, insetInlineEnd: { default: null, ':is([data-orientation="vertical"])': 0, ':is([data-orientation="horizontal"])': "var(--scrollbar-margin)", }, insetInlineStart: { default: null, ':is([data-orientation="horizontal"])': "var(--scrollbar-margin)", }, }, scrollbarReveal: { opacity: { default: null, ":is([data-hover], [data-hovering], [data-scrolling])": 1, }, }, thumb: { width: { default: "100%", ':is([data-orientation="horizontal"])': "var(--scroll-area-thumb-width)", }, height: { default: "var(--scroll-area-thumb-height)", ':is([data-orientation="horizontal"])': "100%", }, }, viewport: { overflow: "auto" },});
// 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 — Base 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 `render`. */export const ScrollArea = { Root: withProvider(BaseScrollArea.Root, "root", "ScrollAreaRoot"), Viewport: withContext( BaseScrollArea.Viewport, () => [scrollAreaViewport.base, chrome.viewport], "ScrollAreaViewport", ), Content: withContext(BaseScrollArea.Content, "content", "ScrollAreaContent"), Scrollbar: withContext( BaseScrollArea.Scrollbar, (v) => [ ...resolveSlotStyles(scrollAreaSlotRecipe.slots.scrollbar, v), chrome.scrollbar, v.variant === "hover" && chrome.scrollbarReveal, ], "ScrollAreaScrollbar", ), Thumb: withContext( BaseScrollArea.Thumb, () => [scrollAreaThumb.base, chrome.thumb], "ScrollAreaThumb", ), Corner: withContext(BaseScrollArea.Corner, "corner", "ScrollAreaCorner"),};