/* * ScrollArea recipe — visual treatment for a custom-scrollbar * container. The headless Soma layer owns the scroll math, the * visibility timer (`type='hover' | 'scroll' | 'auto' | 'always'`) * and the thumb position; Eidos paints the chrome. * * Architecture mirrors Radix Primitives / Bits UI: * * [data-scroll-area] → outer relative-positioned shell * [data-scroll-area-viewport] → scroll container (overflow: auto) * [data-scroll-area-scrollbar] → track (vertical / horizontal) * [data-scroll-area-thumb] → draggable position indicator * [data-scroll-area-corner] → square where both bars meet * * Tokens stay close to the canonical UIX scale: track and thumb * borrow from the neutral palette; the corner tints with the same * track color so a dual-axis container reads as a single unit. */ [data-scroll-area] { position: relative; overflow: hidden; border-radius: var(--scroll-area-radius, var(--radius-md)); background: var(--scroll-area-bg, transparent); } [data-scroll-area-viewport] { position: absolute; inset: 0; /* Trap wheel/touch scroll at the viewport's edges so reaching the top or bottom of the list doesn't chain to the page (``) and scroll it. Both viewport variants (fixed-fill above, autosize below) inherit this — the autosize rule only overrides position/overflow, not this. Mirrors the `overscroll-behavior: contain` every other scroll region already sets (command / listbox / tree-view / table / virtual-list …). */ overscroll-behavior: contain; } /* Autosize mode — content-sized up to a max, then scroll, instead of the default fixed-height fill. The viewport goes in-flow with a `max-block-size` (the consumer sets `--scroll-area-auto-max-block`); the root content-sizes to it and the overlay scrollbars span that box. Enables content-sized popups (combobox / select / menu) — the case the default fixed-height model (and Radix's ScrollArea, which can't take `max-height`) can't cover. Mirrors Mantine's `ScrollArea.Autosize`. */ [data-scroll-area][data-autosize] [data-scroll-area-viewport] { position: static; inset: auto; max-block-size: var(--scroll-area-auto-max-block, none); overflow: auto; } /* The viewport scrolls; the scrollbars are positioned absolutely over it (macOS-style overlay) so they don't displace content. The soma-tracked thumb position drives the inline transform. */ [data-scroll-area-scrollbar] { position: absolute; display: flex; user-select: none; touch-action: none; padding: var(--scroll-area-scrollbar-padding, var(--space-0-5)); background: var(--scroll-area-track-bg, color-mix(in srgb, var(--color-neutral-track) 60%, transparent)); transition: background var(--scroll-area-transition-duration, var(--duration-fast)) var(--scroll-area-transition-ease, var(--ease-default)), opacity var(--scroll-area-transition-duration, var(--duration-fast)) var(--scroll-area-transition-ease, var(--ease-default)); } [data-scroll-area-scrollbar][data-orientation='vertical'] { inset-block: 0; inset-inline-end: 0; inline-size: var(--scroll-area-scrollbar-size, var(--space-2-5)); flex-direction: column; } [data-scroll-area-scrollbar][data-orientation='horizontal'] { inset-inline: 0; inset-block-end: 0; block-size: var(--scroll-area-scrollbar-size, var(--space-2-5)); flex-direction: row; } /* Reserve space on the corresponding edges when each scrollbar is mounted. * Using `:has` lets us avoid an extra wrapper while still keeping the * viewport's scroll content out from under the scrollbars. */ [data-scroll-area]:has([data-scroll-area-scrollbar][data-orientation='vertical']) [data-scroll-area-scrollbar][data-orientation='horizontal'] { inset-inline-end: var(--scroll-area-scrollbar-size, var(--space-2-5)); } [data-scroll-area]:has([data-scroll-area-scrollbar][data-orientation='horizontal']) [data-scroll-area-scrollbar][data-orientation='vertical'] { inset-block-end: var(--scroll-area-scrollbar-size, var(--space-2-5)); } [data-scroll-area-scrollbar][data-state='hidden'] { opacity: 0; pointer-events: none; } [data-scroll-area-scrollbar][data-state='visible'] { opacity: 1; } [data-scroll-area-scrollbar][data-hover], [data-scroll-area-scrollbar][data-dragging] { background: var(--scroll-area-track-bg-hover, color-mix(in srgb, var(--color-neutral-track) 84%, transparent)); } [data-scroll-area-scrollbar]:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 0; } [data-scroll-area-thumb] { flex: 1; border-radius: var(--scroll-area-thumb-radius, var(--radius-full)); background: var(--scroll-area-thumb-bg, var(--color-content-muted)); transition: background var(--scroll-area-transition-duration, var(--duration-fast)) var(--scroll-area-transition-ease, var(--ease-default)); min-inline-size: var(--scroll-area-thumb-min-size, var(--space-3)); min-block-size: var(--scroll-area-thumb-min-size, var(--space-3)); } [data-scroll-area-thumb]:hover, [data-scroll-area-scrollbar][data-dragging] [data-scroll-area-thumb] { background: var(--scroll-area-thumb-bg-hover, var(--color-content-secondary)); } [data-scroll-area-corner] { position: absolute; inset-block-end: 0; inset-inline-end: 0; inline-size: var(--scroll-area-scrollbar-size, var(--space-2-5)); block-size: var(--scroll-area-scrollbar-size, var(--space-2-5)); background: var(--scroll-area-track-bg, color-mix(in srgb, var(--color-neutral-track) 60%, transparent)); } [data-scroll-area-corner][data-state='hidden'] { opacity: 0; }