diff --git a/src/uix/eidos/components/scroll-area/index.ts b/src/uix/eidos/components/scroll-area/index.ts
index a8229c0bc..052ef86e3 100644
--- a/src/uix/eidos/components/scroll-area/index.ts
+++ b/src/uix/eidos/components/scroll-area/index.ts
@@ -40,5 +40,8 @@ export type {
ScrollAreaScrollbarProps as ScrollbarProps,
ScrollAreaThumbProps as ThumbProps,
ScrollAreaCornerProps as CornerProps,
- ScrollAreaType
+ ScrollAreaType,
+ ScrollAreaSize,
+ ScrollAreaRadius,
+ ScrollAreaScrollbars
} from './types';
diff --git a/src/uix/eidos/components/scroll-area/scroll-area.svelte b/src/uix/eidos/components/scroll-area/scroll-area.svelte
index a702a3d7f..7a22e424c 100644
--- a/src/uix/eidos/components/scroll-area/scroll-area.svelte
+++ b/src/uix/eidos/components/scroll-area/scroll-area.svelte
@@ -1,23 +1,81 @@
-
+
{@render children?.()}
+ {#if scrollbars === 'vertical' || scrollbars === 'both'}
+
+
+
+ {/if}
+ {#if scrollbars === 'horizontal' || scrollbars === 'both'}
+
+
+
+ {/if}
+ {#if scrollbars === 'both'}
+
+ {/if}
diff --git a/src/uix/eidos/components/scroll-area/types.ts b/src/uix/eidos/components/scroll-area/types.ts
index d56a8ff92..0705211e3 100644
--- a/src/uix/eidos/components/scroll-area/types.ts
+++ b/src/uix/eidos/components/scroll-area/types.ts
@@ -10,28 +10,62 @@ import type {
/**
* Eidos ScrollArea types.
*
- * Compound shape mirrors Radix Primitives:
+ * Compound shape mirrors Radix Primitives, with three recipe-level
+ * sugar props promoted on the root (`size`, `radius`, `scrollbars`)
+ * that match Radix Themes / Chakra UI conventions:
*
+ *
+ * {content}
+ *
+ *
+ *
*
* {content}
*
*
*
- *
- *
- *
*
*
*
- * Eidos adds no evaluative props — geometry and scrollbar timing live
- * fully in the Soma headless layer. The recipe consumes the
- * `data-state` / `data-orientation` markers emitted by the morfo
- * runtime so visibility transitions and orientation swaps require no
- * additional wiring from Eidos.
+ * Soma still owns geometry + timer; Eidos adds size / radius / axis
+ * sugar on top.
+ */
+
+/** Scrollbar thickness — t-shirt scale (Chakra-style). Maps to a `--space-*` token. */
+export type ScrollAreaSize = 'xs' | 'sm' | 'md' | 'lg';
+
+/** Thumb / corner radius — maps to a `--radius-*` token. */
+export type ScrollAreaRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
+
+/**
+ * Which scrollbars the recipe auto-mounts. When set, the user does
+ * NOT compose `` / `` /
+ * `` manually — the root wires them. Leave
+ * undefined for the canonical Radix-style explicit composition.
+ *
+ * Mirrors Radix Themes' `scrollbars` prop.
*/
+export type ScrollAreaScrollbars = 'vertical' | 'horizontal' | 'both';
-/** Props for the `` root. Pass-through from headless. */
-export type ScrollAreaProps = ProviderProps;
+/** Props for the `` root. Soma props + eidos recipe sugar. */
+export type ScrollAreaProps = ProviderProps & {
+ /**
+ * Scrollbar thickness. Maps to `--scroll-area-scrollbar-size`.
+ * @default 'md'
+ */
+ size?: ScrollAreaSize;
+ /**
+ * Thumb / corner radius. Maps to `--scroll-area-thumb-radius`.
+ * @default 'full'
+ */
+ radius?: ScrollAreaRadius;
+ /**
+ * Which scrollbars the recipe auto-mounts. When omitted, the user
+ * composes Scrollbar / Thumb / Corner parts manually inside
+ * `children` (Radix Primitives style).
+ */
+ scrollbars?: ScrollAreaScrollbars;
+};
/** Props for ``. Pass-through from headless. */
export type ScrollAreaViewportProps = ViewportProps;
/** Props for ``. Pass-through from headless. */
diff --git a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts
index 88d2534a0..cac5f254b 100644
--- a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts
+++ b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts
@@ -230,7 +230,12 @@ export class ScrollAreaScrollbarProvider {
// Visibility state
visible = $state(false);
private hideTimer: TimerHandle | null = null;
- private mounted = false;
+ // `mounted` MUST be reactive — `shouldShow` reads it to suppress the
+ // initial-mount flash, so flipping it from false→true during the
+ // first frame has to invalidate the derived. Without `$state` the
+ // derived caches the false value and `visible` flips inside the
+ // `shouldShow` derivation lose the race.
+ private mounted = $state(false);
hovering = $state(false);
dragging = $state(false);
@@ -259,10 +264,20 @@ export class ScrollAreaScrollbarProvider {
});
// Reveal the scrollbar on parent scroll for 'scroll' and 'hover' types.
- // Previously lived in the wrapper as an ad-hoc effect.
+ // The first run of an `$effect` is a subscription-setup pass, not a real
+ // scroll event — without the `firstRun` guard the initial run would call
+ // `show()` immediately, setting `visible=true` before `mounted` flips.
+ // Subsequent real-scroll runs would then see `visible` already at `true`
+ // and the no-op assignment wouldn't invalidate `shouldShow`, leaving the
+ // bar stuck hidden until the hide timer expired.
+ let firstRun = true;
$effect(() => {
void this.provider.scrollTop;
void this.provider.scrollLeft;
+ if (firstRun) {
+ firstRun = false;
+ return;
+ }
const type = this.provider.opts.type.current;
if (type === 'scroll' || type === 'hover') {
this.show();
diff --git a/web/routes/uix/components/scroll-area/+page.svelte b/web/routes/uix/components/scroll-area/+page.svelte
index 25f7435c7..641c5ad4e 100644
--- a/web/routes/uix/components/scroll-area/+page.svelte
+++ b/web/routes/uix/components/scroll-area/+page.svelte
@@ -2,7 +2,10 @@
import {
ScrollArea,
type ScrollAreaProps,
- type ScrollAreaType
+ type ScrollAreaType,
+ type ScrollAreaSize,
+ type ScrollAreaRadius,
+ type ScrollAreaScrollbars
} from '$uix/eidos/components/scroll-area';
import { compileMorfo } from '$uix/morfo';
import { scrollAreaMorfo } from '@/uix/morfo/components/scroll-area';
@@ -17,14 +20,23 @@
const types: ScrollAreaType[] = ['hover', 'scroll', 'auto', 'always'];
let type = $state('hover');
let scrollHideDelay = $state(600);
- type Axis = 'vertical' | 'horizontal' | 'both';
- const axes = ['vertical', 'horizontal', 'both'] as const;
+
+ const sizes: ScrollAreaSize[] = ['xs', 'sm', 'md', 'lg'];
+ let size = $state('md');
+
+ const radii: ScrollAreaRadius[] = ['none', 'sm', 'md', 'lg', 'full'];
+ let radius = $state('full');
+
+ type Axis = ScrollAreaScrollbars;
+ const axes: Axis[] = ['vertical', 'horizontal', 'both'];
let axis = $state('vertical');
let itemCount = $state(40);
const areaProps = $derived>({
type,
- scrollHideDelay
+ scrollHideDelay,
+ size,
+ radius
});
// ── Compiled morfo ───────────────────────────────────────────────────
@@ -61,6 +73,8 @@
);
// ── Snippets ─────────────────────────────────────────────────────────
+ // Soma is headless — no `size` / `radius` / `scrollbars` sugar; the user
+ // composes Scrollbar / Thumb / Corner parts directly.
const somaSnippet = $derived(
[
"