Layout · ScrollArea

ScrollArea

Custom-scrollbar container. Hides the native scrollbar and renders a custom-painted track + thumb on top of a scrollable viewport. The headless soma layer owns the scroll math, the visibility timer (type) and the thumb position; Eidos paints chrome only. Compound API mirrors Radix Primitives: explicit Viewport / Scrollbar / Thumb / Corner composition.

parts{compiled.parts.order.length} events{events.length} type{type} scope{scrollAreaMorfo.scope.join(', ')}

Sample list — {itemCount} items

Scroll inside the framed box to observe the custom scrollbar painted by Eidos. The thumb follows the soma-tracked scroll position; the track hover state is driven by the morfo's data-hover marker.

    {#each sampleItems as item}
  • {item} — body text continues across the line for horizontal scroll demos when the axis is wide enough.
  • {/each}
trace {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} · type {type} axis {axis} · hideDelay {scrollHideDelay}ms
{#if tab === 'live'}

Controls

All knobs are soma props — the headless layer owns visibility timing and scroll math. The eidos layer paints chrome with no additional props; tweak the recipe via the --scroll-area-* custom properties.

soma props · headless behavior
eidos props · recipe sugar
Composition · which scrollbars to mount
soma headless · scroll math + visibility timer svelte
{somaSnippet}
eidos visual · pure pass-through of soma; recipe paints chrome svelte
{eidosSnippet}
{/if} {#if tab === 'api'}

API reference

Pass-through from soma — no evaluative additions on the eidos layer. Visual tuning lives in the recipe via --scroll-area-* custom properties.

ScrollArea (root)
PropTypeDefaultDescription
type {types.map((t) => `'${t}'`).join(' | ')} 'hover' Visibility behavior of the scrollbars.
scrollHideDelay number 600 ms before hiding (only for 'hover' and 'scroll').
dir 'ltr' | 'rtl' from Soma Text direction. Affects horizontal scrollbar behavior.
ScrollArea.Scrollbar
PropTypeDefaultDescription
orientation 'vertical' | 'horizontal' — Required. Drives the recipe's data-orientation selector.
forceMount boolean false Keep in DOM when hidden. Enables CSS transitions.
CSS tokens
TokenDefaultNotes
--scroll-area-scrollbar-size var(--space-2-5) Inline-axis size of the track (and block-axis for horizontal).
--scroll-area-track-bg color-mix(--color-neutral-track) Track background, also used by the Corner.
--scroll-area-thumb-bg var(--color-content-muted) Thumb fill; hover state lifts to --color-content-secondary.
--scroll-area-thumb-min-size var(--space-3) Floor on the thumb so it stays grabable even on long content.
Reference comparison
LibraryClosest equivalentDifference
radix-primitives <ScrollArea> Identical compound shape. UIX adds bare token names + soma layer.
bits-ui <ScrollArea> Same — UIX matches the Svelte-native shape.
react-aria-components n/a (uses native scrolling + virtualization) UIX paints chrome; React Aria leaves it to the OS / browser.
{/if} {#if tab === 'morfo'}

Morfo contract

Source: src/uix/morfo/components/scroll-area.ts. Scope is ['soma'] — the headless layer is the source of state. Five parts; no events (see Sema tab).

FieldValue
name{scrollAreaMorfo.name}
kebab{scrollAreaMorfo.kebab}
scope{scrollAreaMorfo.scope.join(', ')}
parts{partsList.length}
events0
Parts
{#each partsList as part} {/each}
kebab marker element role archetype states optional
{part.kebab} [{part.marker}] <{part.defaultElement}> {part.role ?? '—'} {part.archetype ?? '—'} {part.states.length ? part.states.join(' | ') : '—'} {part.optional ? 'yes' : 'no'}
{#each scrollAreaMorfo.parts as rawPart} {@const partAny = rawPart as unknown as { kebab: string; aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string; }>; }} {@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []} {@const ariaAttrs = partAny.aria ?? []} {#if dataAttrs.length || ariaAttrs.length}
{partAny.kebab}
{#if dataAttrs.length}
{#each dataAttrs as attr} {/each}
data-attrValuesSource kind
{attr.attr} {attr.values ? attr.values.join(' | ') : '—'} {'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}
{/if} {#if ariaAttrs.length}
{#each ariaAttrs as a} {/each}
aria-attrSource kindSeverity
{a.attr} {a.value.kind} {a.severity ?? 'required'}
{/if} {/if} {/each}
{/if} {#if tab === 'sema'}

sema · events

ScrollArea declares no semantic events. Scrolling is continuous and non-evaluative — there is no commit / emerge / contact verb to project onto a perceptual channel. The Scrollbar's data-state visibility transition is a CSS concern, not a sema event. Consumers that need to react to scroll position read data-at-top / data-at-bottom / data-at-left / data-at-right on the Viewport — see the morfo declaration above.

{/if} {#if tab === 'recipe'}

Eidos recipe

Recipe lives in src/uix/eidos/components/scroll-area/scroll-area.css. The data-orientation attribute drives the geometry of the scrollbar; the visibility states (data-state='visible' / 'hidden') come from the soma timer.

SelectorOwnerPurpose
[data-scroll-area] morfo Provider marker. Sets up the relative-positioned shell.
[data-scroll-area-viewport] morfo Viewport marker. Receives overflow: auto from soma.
[data-scroll-area-scrollbar][data-orientation='vertical'] eidos Vertical track sizing.
[data-scroll-area-scrollbar][data-orientation='horizontal'] eidos Horizontal track sizing.
[data-scroll-area-scrollbar][data-state='hidden'] eidos Visibility fade for the auto-hide timer.
[data-scroll-area-thumb] eidos Painted thumb; position is set inline by soma.
[data-scroll-area-corner] eidos Square between the two scrollbars when both axes scroll.
{/if} {#if tab === 'a11y'}

Accessibility

The custom scrollbar must remain reachable to keyboard and screen-reader users. The morfo stamps the WAI-ARIA scrollbar role on every scrollbar with aria-valuenow / aria-valuemin / aria-valuemax bound to the soma-tracked scroll percentage.

ARIA contract
PartAttributeValue
scrollbarrole'scrollbar'
scrollbararia-controls id of viewport
scrollbararia-orientation 'vertical' | 'horizontal'
scrollbararia-valuenow / min / max scroll percent (0–100)
scrollbararia-label 'Vertical scrollbar' / 'Horizontal scrollbar'
Other concerns
ConcernContract
Keyboard The viewport accepts native scroll keys (Arrow / Page / Home / End) when focused. Browser keyboard handling is preserved; UIX does not steal arrow keys.
Focus visible :focus-visible on the scrollbar exposes the canonical UIX focus ring.
Touch The Scrollbar declares touch-action: none so touch drags on the custom thumb don't conflict with browser-native gestures.
RTL Soma's dir prop is honoured: in RTL, the vertical scrollbar moves to the inline-start side, matching native scrollbar positioning.
{/if}