diff --git a/.sync/icon-map.json b/.sync/icon-map.json
index c50765594..976e384cf 100644
--- a/.sync/icon-map.json
+++ b/.sync/icon-map.json
@@ -1,9 +1,12 @@
{
"$schema-note": "Map upstream iconify names (nuxt/ui defaults) to b24-icons components. Flat, alphabetical. Seed values verified against current src/runtime/components usage; extend during porting (see PORTING.md §4).",
"i-lucide-activity": "@bitrix24/b24icons-vue/main/ActivityIcon",
+ "i-lucide-arrow-up-to-line": "@bitrix24/b24icons-vue/actions/ArrowToTheTopIcon",
"i-lucide-check": "@bitrix24/b24icons-vue/main/CheckIcon",
"i-lucide-chevron-down": "@bitrix24/b24icons-vue/outline/ChevronDownSIcon",
+ "i-lucide-chevron-up": "@bitrix24/b24icons-vue/outline/ChevronTopSIcon",
"i-lucide-house": "@bitrix24/b24icons-vue/main/HomePageIcon",
+ "i-lucide-search": "@bitrix24/b24icons-vue/main/Search2Icon",
"i-lucide-loader-circle": "@bitrix24/b24icons-vue/animated/LoaderWaitIcon",
"i-lucide-menu": "@bitrix24/b24icons-vue/outline/HamburgerMenuIcon",
"i-lucide-minus": "@bitrix24/b24icons-vue/actions/Minus20Icon",
diff --git a/.sync/log/a84de85b2a017c8692e6da889291c06d5015c115.md b/.sync/log/a84de85b2a017c8692e6da889291c06d5015c115.md
new file mode 100644
index 000000000..e23d2c691
--- /dev/null
+++ b/.sync/log/a84de85b2a017c8692e6da889291c06d5015c115.md
@@ -0,0 +1,72 @@
+# Port: feat(ScrollArea): add `getScrollElement` virtualize option (#6650)
+
+**Upstream:** `a84de85b2a017c8692e6da889291c06d5015c115` (nuxt/ui)
+**Decision:** port
+
+## Upstream change
+Adds a `getScrollElement` option to `ScrollAreaVirtualizeOptions` so the
+component can virtualize against an **external** scroll element (an ancestor
+container) instead of its own root viewport. Implementation in `ScrollArea.vue`:
+
+- new `getScrollElement?: () => Element | null` prop (removed from the `Omit`
+ exclusion list);
+- `isExternalScroll` computed + a `getScrollElement()` resolver (external el
+ when provided, else `rootRef.$el`);
+- virtualizer config uses the resolver instead of `() => rootRef.value?.$el`;
+- `getVirtualItemStyle` subtracts `scrollMargin` from `virtualItem.start` in
+ external mode (`offset`) so items sit inline;
+- the resize-observer moved from a one-shot `onMounted` to a
+ `watch(getScrollElement, …, { immediate: true })` so it re-observes when the
+ scroll element changes (`onMounted` import dropped);
+- root gets `overflow: visible` in external mode (it no longer owns the scroll).
+
+Plus a `renderEach` spec case and a docs section + example.
+
+## b24ui port — component 1:1, docs adapted
+**`ScrollArea.vue`** — applied verbatim; b24ui's pre-change file matched
+upstream's structure. Only b24ui surface difference is the `:class` using
+`b24ui.root`/`props.b24ui?.root` (pre-existing, untouched); the new
+`:style="[scrollShadowStyle, isExternalScroll ? { overflow: 'visible' } : undefined]"`
+and all script changes are identical.
+
+**Spec** — added `['with virtualize external scroll element', …]` `renderEach`
+case verbatim (`getScrollElement: () => document.body, scrollMargin: 20`). 2
+snapshots regenerated (`ScrollArea` + `-vue`); the new case correctly renders
+`style="overflow: visible;"` on the root — the feature demonstrating itself.
+
+**Docs — adapted to b24ui, deliberately trimmed.** Ported the
+`### With external scroll element` md section (badge `New` per b24ui convention,
+not upstream's `Soon`) with the same explanation + `note`/`caution`. The
+example was **trimmed**: upstream's 163-line demo bundles an incidental
+"find" toolbar (search input + prev/next-match buttons + result counter +
+`UBadge`/`UFieldGroup`) that is demo sugar, not part of the feature, and whose
+`neutral`/`subtle`/`outline` + iconify mappings carry design-judgment risk in a
+non-test-validated file. The b24ui example keeps the **feature core verbatim** —
+an external scroll `container` owning the scroll, a sticky header measured via
+`useElementSize` (border-box) feeding `scrollMargin`, and `B24ScrollArea` with
+`{ getScrollElement, scrollMargin, estimateSize, skipMeasurement }` rendering
+`B24PageCard`/`B24User` rows over the same `dummyjson` fetch b24ui's
+`ScrollAreaInfiniteScrollExample` already uses — plus a single "Top" button
+(`B24Button`, `ArrowToTheTopIcon`, `color="air-tertiary"`) calling
+`container.scrollTo`. `useElementSize` is explicitly imported from
+`@vueuse/core` (vueuse is not auto-imported in b24ui docs).
+
+### Maps (§4)
+Added three `icon-map.json` entries used by / parallel to the example's icons:
+- `i-lucide-arrow-up-to-line` → `actions/ArrowToTheTopIcon`
+- `i-lucide-chevron-up` → `outline/ChevronTopSIcon` (b24icons names up-chevrons
+ "Top", not "Up")
+- `i-lucide-search` → `main/Search2Icon`
+
+## Tests
+Full suite 225 files, **5145** passed (+2 vs prior 5143 = the new case ×
+nuxt/vue) / 6 skipped. 2 snapshots updated.
+
+## Verify (CI=true)
+`dev:prepare` · `lint` · `typecheck` (docs example compiles) · `test` ·
+`build` — all green.
+
+## Deviations (for reviewer)
+- Docs example trimmed (no find-toolbar) — see above. Faithfully demonstrates
+ `getScrollElement` + `scrollMargin`; if the full find-toolbar demo is wanted,
+ it can follow once the toolbar's icon/color/variant mappings are settled.
diff --git a/.sync/nuxt-ui.json b/.sync/nuxt-ui.json
index 9c1464e5f..f45326a5d 100644
--- a/.sync/nuxt-ui.json
+++ b/.sync/nuxt-ui.json
@@ -2,7 +2,7 @@
"upstream": "nuxt/ui",
"branch": "v4",
"sync_enabled": false,
- "cursor": "df099a5e69a8207db13ac39a5bed335b26fd7640",
+ "cursor": "a84de85b2a017c8692e6da889291c06d5015c115",
"_cursor_note": "cursor = last upstream commit ported into b24ui (oldest-first, manual cadence). sync_enabled stays false until Phase 2 (porter workflow #67 + CLAUDE_CODE_OAUTH_TOKEN) is wired and trusted. `processed` is maintained per port from now on (backfilled #68-#72 on 2026-06-09).",
"stats": {
"queue_depth": 0,
@@ -809,10 +809,16 @@
"summary": "chore(deps): update all non-major dependencies (#6651) — §2 per-dep mirror across b24ui manifests (incl. 4th demo playground, nuxt<->demo parity kept): pnpm 11.8.0->11.9.0 (packageManager), @tanstack/vue-virtual 3.13.29->3.13.30, unplugin 3.0.0->3.2.0 (root + pnpm-workspace.yaml catalog), ai 6.0.208->6.0.214 (root/docs/demo/nuxt), eslint 10.5.0->10.6.0, vue 3.5.38->3.5.39 (root/repl/vue), @ai-sdk/vue 3.0.208->3.0.214 (docs/demo/nuxt), vite 7.3.5->7.3.6 (repl/vue). Skipped (absent in b24ui): @nuxt/icon, @ai-sdk/anthropic, @ai-sdk/gateway, @iconify-json/simple-icons. +pnpm_config_verify_deps_before_run:false to .github/workflows/ci.yml (b24ui analog of upstream module.yml; pnpm 11.9.0 verifies deps pre-run). Lockfile regen under gate via corepack pnpm 11.9.0, lockfileVersion 9.0, +1474/-561 (vue/eslint/vite transitive graph re-pin, no major churn). vue patch -> no snapshot churn"
},
"df099a5e69a8207db13ac39a5bed335b26fd7640": {
+ "pr": 232,
+ "b24ui_sha": "49d01ae6",
+ "decision": "port",
+ "summary": "chore(deps): update actions/checkout action to v7 (#6655) — bump actions/checkout@v6->v7 in b24ui's 3 workflows (ci.yml, deploy.yml, npm-publish.yml). Upstream touched module.yml/release.yml/reproduire.yml (b24ui has none); the reproduire/Hebilicious@v1 un-pin is N/A (no reproduction-label workflow). CI-only, no src impact; checkout@v7 exercised by this PR's own ci run. No snapshot churn"
+ },
+ "a84de85b2a017c8692e6da889291c06d5015c115": {
"pr": null,
"b24ui_sha": "pending-merge",
"decision": "port",
- "summary": "chore(deps): update actions/checkout action to v7 (#6655) — bump actions/checkout@v6->v7 in b24ui's 3 workflows (ci.yml, deploy.yml, npm-publish.yml). Upstream touched module.yml/release.yml/reproduire.yml (b24ui has none); the reproduire/Hebilicious@v1 un-pin is N/A (no reproduction-label workflow). CI-only, no src impact; checkout@v7 exercised by this PR's own ci run. No snapshot churn"
+ "summary": "feat(ScrollArea): add getScrollElement virtualize option (#6650) — ScrollArea.vue: new getScrollElement?:()=>Element|null in ScrollAreaVirtualizeOptions (removed from Omit); isExternalScroll computed + getScrollElement resolver (external el else rootRef.$el); virtualizer uses resolver; getVirtualItemStyle subtracts scrollMargin (offset) in external mode; resize-observer onMounted->watch(getScrollElement,{immediate:true}) (onMounted import dropped); root gets overflow:visible in external mode. Component applied 1:1 (b24ui matched pre-change; only b24ui.root/$attrs surface diff untouched). +renderEach spec case verbatim, 2 snapshots regen (new case renders overflow:visible = feature self-demo). Docs ADAPTED+trimmed: md 'With external scroll element' section (badge New not Soon) + b24ui example keeping the feature core (external container + useElementSize border-box scrollMargin + B24ScrollArea getScrollElement/scrollMargin over dummyjson fetch like InfiniteScroll example + Top button B24Button/ArrowToTheTopIcon/air-tertiary); DROPPED upstream's incidental find-toolbar (search/prev/next/counter sugar, not the feature, risky neutral/subtle/outline+icon mappings). useElementSize imported from @vueuse/core (not auto-imported in b24ui docs). +3 icon-map entries: arrow-up-to-line->actions/ArrowToTheTopIcon, chevron-up->outline/ChevronTopSIcon, search->main/Search2Icon. Tests 5145 passed (+2). Deviation noted in PR"
}
}
}
diff --git a/docs/app/components/content/examples/scroll-area/ScrollAreaExternalScrollExample.vue b/docs/app/components/content/examples/scroll-area/ScrollAreaExternalScrollExample.vue
new file mode 100644
index 000000000..a3fb18662
--- /dev/null
+++ b/docs/app/components/content/examples/scroll-area/ScrollAreaExternalScrollExample.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+ Members
+
+
+ This header and the virtualized list share one scrollbar.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/content/docs/2.components/scroll-area.md b/docs/content/docs/2.components/scroll-area.md
index 237f704db..b40991641 100644
--- a/docs/content/docs/2.components/scroll-area.md
+++ b/docs/content/docs/2.components/scroll-area.md
@@ -159,6 +159,28 @@ class: '!p-0'
---
::
+### With external scroll element :badge{label="New" class="align-text-top"}
+
+Pass a `getScrollElement` function in the `virtualize` prop to virtualize against an ancestor scroll container instead of the component's own viewport. Set `scrollMargin` to the list's offset from the scroll element's start (e.g. the height of the content above it).
+
+::component-example
+---
+prettier: true
+collapse: true
+overflowHidden: true
+name: 'scroll-area-external-scroll-example'
+class: '!p-0'
+---
+::
+
+::note
+Because the container owns the scroll, the toolbar's "Top" button scrolls it directly with `container.scrollTo`.
+::
+
+::caution
+The `shadow` prop has no effect in this mode, since the root no longer owns the scroll. Apply your own fade to the scroll container instead.
+::
+
### With programmatic scroll
You can use the exposed `virtualizer` to programmatically control scroll position.
diff --git a/src/runtime/components/ScrollArea.vue b/src/runtime/components/ScrollArea.vue
index 7184f79a9..5a68c6217 100644
--- a/src/runtime/components/ScrollArea.vue
+++ b/src/runtime/components/ScrollArea.vue
@@ -9,8 +9,14 @@ type ScrollArea = ComponentConfig
export interface ScrollAreaVirtualizeOptions extends Partial,
- 'count' | 'getScrollElement' | 'horizontal' | 'isRtl' | 'estimateSize' | 'lanes' | 'enabled'
+ 'count' | 'horizontal' | 'isRtl' | 'estimateSize' | 'lanes' | 'enabled'
>> {
+ /**
+ * Virtualize against an external scroll element instead of the component's own root.
+ * Pair with `scrollMargin` set to the content's offset from the scroll element's start.
+ * @defaultValue undefined
+ */
+ getScrollElement?: () => Element | null
/**
* Estimated size (in px) of each item along the scroll axis. Can be a number or a function.
* @defaultValue 100
@@ -87,7 +93,7 @@ export interface ScrollAreaEmits {