\n \n \n\n\n
\n
\n \n
\n
\n \n
\n
\n\n \n\n\n```\n\nBoom—scroll one, both move. **Alpine.js computed values**? Add `scrollPos: 0` and `$watch('scrollPos', val => { this.editor2.scrollTop = val; })`, but events suffice for most.\n\nTested this? Line up matching text; cursors stay synced. For diffs, highlight changes—scroll locks 'em tight.\n\n---\n\n## Toolbar Visibility and Fixes {#toolbar-fixes}\n\nToolbars hiding? Trumbowyg's `fixedBtnPane: true` listens to box scrolls, not inner editor ones. Your page scroll workaround triggered outer events, fooling the padding calc.\n\nFix: Ensure listeners are strictly on `.trumbowyg-editor`. If toolbars still dip, force the padding refresh:\n\n```javascript\nconst refreshToolbar = (box) => {\n const toolbar = box.querySelector('.trumbowyg-button-pane');\n box.style.paddingTop = toolbar.offsetHeight + 'px';\n};\n```\n\nCall after sync. Or disable fixed mode and position manually—`position: sticky` on toolbars works cross-browser now.\n\nEdge cases? Mobile touch-scrolls lag? Throttle with lodash or native:\n\n```javascript\nlet ticking = false;\nfunction throttledSync(source, target) {\n if (!ticking) {\n requestAnimationFrame(() => {\n target.scrollTop = source.scrollTop;\n ticking = false;\n });\n ticking = true;\n }\n}\n```\n\n**Sync scroll** feels native. Resize windows? Add `@resize.window` in Alpine to rebind refs.\n\n---\n\n## Advanced Library Option {#syncscroll-lib}\n\nWant zero boilerplate? Drop in [syncscroll library](https://github.com/asvd/syncscroll)—946 bytes, vanilla JS. Just add `class=\"syncscroll\" name=\"mygroup\"` to both `.trumbowyg-editor` divs post-init:\n\n```javascript\nsyncscroll({groups: [{name: 'mygroup'}]});\n```\n\nIt handles multi-element sync out-of-box, even horizontal if needed. Like [diff2html's side-by-side](https://github.com/rtfpessoa/diff2html), but pure scroll. Integrate with **Alpine.js** via `x-init`.\n\nOverkill for two editors? Maybe. But scales if you add more panes. No perf hit—micro indeed.\n\n---\n\n## Sources {#sources}\n\n1. [Stack Overflow: Sync scroll between 2 textarea editors](https://stackoverflow.com/questions/79860126/sync-scroll-between-2-texarea-editors) \n2. [GitHub: asvd/syncscroll](https://github.com/asvd/syncscroll) \n3. [.NET Curry: Synchronize Scrolling of Two Multiline TextBoxes](https://www.dotnetcurry.com/ShowArticle.aspx?ID=422) \n4. [Trumbowyg Documentation](https://alex-d.github.io/Trumbowyg/documentation/) \n5. [Alpine.js: on directive](https://alpinejs.dev/directives/on) \n6. [GitHub: rtfpessoa/diff2html](https://github.com/rtfpessoa/diff2html)\n\n---\n\n## Conclusion {#conclusion}\n\nSyncing **Trumbowyg** editors with **Alpine.js** boils down to targeting `.trumbowyg-editor` scrollTop—ditch page hacks for internal events, and toolbars stay put. Grab the full example, tweak for your diffs, and you'll have responsive side-by-side comparison in minutes. Vanilla works too; scale with syncscroll if needed. Questions on **alpine js computed values** for positions? It's reactive magic waiting."},{"name":"Sync vertical scrolling between two Trumbowyg editors with Alpine.js","step":[{"name":"Initialize Trumbowyg on both textareas with fixedBtnPane enabled","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":1},{"name":"After initialization, select each .trumbowyg-editor contenteditable div from the DOM or via $refs","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":2},{"name":"Attach scroll listeners to both .trumbowyg-editor elements to read scrollTop","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":3},{"name":"Mirror the source editor's scrollTop to the target inside requestAnimationFrame and use a syncing flag to avoid infinite loops","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":4},{"name":"Refresh toolbar padding or use position:sticky to keep the toolbar visible during internal scrolling","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":5},{"name":"Optionally integrate the lightweight syncscroll library to scale syncing to multiple panes","@type":"HowToStep","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","position":6}],"@type":"HowTo","@context":"https://schema.org","description":"Step-by-step instructions to synchronize internal vertical scroll of two Trumbowyg instances via Alpine.js or vanilla JavaScript while keeping toolbars visible.","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js"},"inLanguage":"en","dateCreated":"2026-01-05T16:35:48.554Z","datePublished":"2026-01-05T16:35:48.554Z","dateModified":"2026-01-05T16:35:48.554Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","url":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js"},{"@type":"CollectionPage","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js/#related-questions","name":"Alpine.js: Sync Trumbowyg Vertical Scrolling with Code","description":"Sync vertical scrolling between two Trumbowyg editors using Alpine.js or vanilla JS. Keeps toolbars visible and avoids page scrolling. Includes runnable code.","url":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js","inLanguage":"en","mainEntity":{"@type":"ItemList","@id":"https://neuroanswers.net/c/web/q/sync-trumbowyg-editors-alpine-js/#related-questions","itemListElement":[{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/how-to-get-current-url-javascript","name":"Get Current URL in JavaScript: window.location.href","position":1,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/how-to-get-current-url-javascript","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/how-to-get-current-url-javascript"},"inLanguage":"en","dateCreated":"2025-10-21T16:05:37.502Z","datePublished":"2025-10-21T16:05:37.502Z","dateModified":"2026-01-08T10:49:32.704Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"Get Current URL in JavaScript: window.location.href","description":"Learn how to get the full current URL in JavaScript using window.location.href and document.URL. Capture on page load, parse parts like protocol, host, pathname, search, hash with examples and pitfalls.","keywords":["window location href","javascript url","get current url","document url javascript","javascript get url","js window location href","javascript window url","js get current url","window location href url"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/react-pre-19-security-vulnerabilities-upgrade-required","name":"React Pre-19 Vulnerabilities: Upgrade to 19 Required?","position":2,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/react-pre-19-security-vulnerabilities-upgrade-required","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/react-pre-19-security-vulnerabilities-upgrade-required"},"inLanguage":"en","dateCreated":"2026-01-01T10:40:50.764Z","datePublished":"2026-01-01T10:40:50.764Z","dateModified":"2026-01-31T12:50:53.812Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"React Pre-19 Vulnerabilities: Upgrade to 19 Required?","description":"No known CVEs in React versions before 19. React 19 fixes address new Server Components issues, not pre-19. Upgrade only if using RSC; stay on patched React 18 for security. Official advisories confirm no pre-19 risks.","keywords":["react 19","react security","cve react","react 18","react vulnerabilities","react server components","react upgrade","react уязвимость","critical security vulnerability react"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/react-window-virtualization-dynamic-lists-media","name":"React Window Virtualization for Dynamic Lists with Media","position":3,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/react-window-virtualization-dynamic-lists-media","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/react-window-virtualization-dynamic-lists-media"},"inLanguage":"en","dateCreated":"2026-01-14T13:56:19.646Z","datePublished":"2026-01-14T13:56:19.646Z","dateModified":"2026-01-15T06:53:59.707Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"React Window Virtualization for Dynamic Lists with Media","description":"Fix layout shakes in react window and react virtualized for dynamic React lists with frequent updates, images, buttons. Code examples, react-virtuoso alternative, patterns for smooth performance.","keywords":["react window","react windows","react virtualized","react virtualization","fixedsizelist react window","react virtualized auto sizer","dynamic list react","react window virtualization","virtualized list react"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/format-javascript-date-10-aug-2010-string","name":"How to Format JavaScript Date to '10-Aug-2010' String","position":4,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/format-javascript-date-10-aug-2010-string","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/format-javascript-date-10-aug-2010-string"},"inLanguage":"en","dateCreated":"2025-10-21T20:59:58.879Z","datePublished":"2025-10-21T20:59:58.879Z","dateModified":"2026-01-05T15:48:27.643Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"How to Format JavaScript Date to '10-Aug-2010' String","description":"Learn javascript date format for '10-Aug-2010' using manual mapping, Intl.DateTimeFormat, or date-fns. Covers js date to string format functions, js string to date parsing, date string javascript examples, and locale-aware options.","keywords":["javascript date format","js date to string format","js string to date","date string javascript","js date to iso string","format date javascript","javascript date to string","intl datetimeformat","date-fns format"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/i18next-attribute-interpolation-laravel-validation","name":"i18next Attribute Interpolation: Laravel-Style Validation Messages","position":5,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/i18next-attribute-interpolation-laravel-validation","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/i18next-attribute-interpolation-laravel-validation"},"inLanguage":"en","dateCreated":"2026-01-20T16:57:19.494Z","datePublished":"2026-01-20T16:57:19.494Z","dateModified":"2026-01-20T16:57:19.494Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"i18next Attribute Interpolation: Laravel-Style Validation Messages","description":"Learn how to implement automatic attribute interpolation in i18next validation error messages similar to Laravel's :attribute placeholder system.","keywords":["i18next","i18next interpolation","attribute interpolation","laravel validation","validation error messages","i18next postprocessor","i18next custom plugins","translation interpolation","i18next js","laravel :attribute"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/focus-jquery-timepicker-validation-fail","name":"Focus jQuery Timepicker on Validation Failure","position":6,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/focus-jquery-timepicker-validation-fail","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/focus-jquery-timepicker-validation-fail"},"inLanguage":"en","dateCreated":"2026-01-01T10:09:09.953Z","datePublished":"2026-01-01T10:09:09.953Z","dateModified":"2026-01-01T10:09:09.953Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"Focus jQuery Timepicker on Validation Failure","description":"Learn how to reliably focus a jQuery Timepicker input field like #Period1TimeFrom when jQuery validation fails. Use setTimeout, event hooks, and custom methods to fix focus issues after form errors.","keywords":["jquery validation","jquery timepicker","focus input field","timepicker focus","form validation fail","settimeout focus","jquery validate timepicker","timepicker validation"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/breaking-out-nested-loops-javascript","name":"Breaking Out of Nested Loops in JavaScript","position":7,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/breaking-out-nested-loops-javascript","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/breaking-out-nested-loops-javascript"},"inLanguage":"en","dateCreated":"2026-01-26T12:15:53.165Z","datePublished":"2026-01-26T12:15:53.165Z","dateModified":"2026-01-29T12:48:36.217Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"Breaking Out of Nested Loops in JavaScript","description":"Learn different approaches to exit nested loops in JavaScript using labeled statements, flags, and other techniques for efficient code execution.","keywords":["javascript break","nested loops","javascript break statement","loop control","exit loops","labeled statements","javascript loops"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/javascript-close-tab-confirmation-dialog","name":"JavaScript Close Tab Confirm: Custom YES/NO Dialog Implementation","position":8,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/javascript-close-tab-confirmation-dialog","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/javascript-close-tab-confirmation-dialog"},"inLanguage":"en","dateCreated":"2026-02-13T10:37:33.571Z","datePublished":"2026-02-13T10:37:33.571Z","dateModified":"2026-02-13T10:37:33.571Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"JavaScript Close Tab Confirm: Custom YES/NO Dialog Implementation","description":"Learn how to create a link that shows a custom YES/NO confirmation dialog before closing the current browser tab with JavaScript.","keywords":["javascript close tab confirm","javascript alert confirm","window close javascript","confirm dialog javascript","browser api javascript","custom confirmation modal","tab closure confirmation"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/form-reloads-despite-preventdefault-fix","name":"Why Form Reloads Despite event.preventDefault() Fix","position":9,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/form-reloads-despite-preventdefault-fix","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/form-reloads-despite-preventdefault-fix"},"inLanguage":"en","dateCreated":"2026-01-09T10:28:35.643Z","datePublished":"2026-01-09T10:28:35.643Z","dateModified":"2026-01-09T10:28:35.643Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"Why Form Reloads Despite event.preventDefault() Fix","description":"Fix page reload on form submit even with event.preventDefault() on button click. Learn to use form submit event listener, handle Enter key, programmatic submit, and debugging steps for reliable prevention.","keywords":["preventdefault","event preventdefault","preventdefault javascript","addeventlistener preventdefault","form submit preventdefault","javascript form submit","prevent form submission","e preventdefault","click preventdefault"],"image":[],"articleBody":""}},{"@type":"ListItem","@id":"https://neuroanswers.net/c/web/q/javascript-delete-remove-property-object","name":"JavaScript Delete: Remove Property from Object","position":10,"item":{"@type":"Article","@id":"https://neuroanswers.net/c/web/q/javascript-delete-remove-property-object","mainEntityOfPage":{"@type":"WebPage","@id":"https://neuroanswers.net/c/web/q/javascript-delete-remove-property-object"},"inLanguage":"en","dateCreated":"2025-10-19T13:52:16.263Z","datePublished":"2025-10-19T13:52:16.263Z","dateModified":"2026-01-08T13:08:19.572Z","author":[{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}}],"publisher":{"@type":"Organization","@id":"https://neuroanswers.net/about","name":"NeuroAnswers","url":"https://neuroanswers.net/about","logo":{"@type":"ImageObject","url":"https://neuroanswers.net/logo.png","width":"512","height":"512"}},"headline":"JavaScript Delete: Remove Property from Object","description":"Learn javascript delete operator, Reflect.deleteProperty, object rest/spread to remove properties. Examples for js delete object, js delete object property, js object delete key without mutating.","keywords":["javascript delete","js delete object","js delete object property","javascript delete object","js object delete key","object methods javascript"],"image":[],"articleBody":""}}]}}]}
Web

Alpine.js: Sync Trumbowyg Vertical Scrolling with Code

Sync vertical scrolling between two Trumbowyg editors using Alpine.js or vanilla JS. Keeps toolbars visible and avoids page scrolling. Includes runnable code.

1 answer 2 views

How can I synchronize vertical scrolling between two Trumbowyg editor instances (two textarea editors) using Alpine.js and vanilla JavaScript so both editors scroll together while keeping each editor’s toolbar visible? Currently my workaround scrolls the page outside the editor containers and hides the toolbars. What is the correct approach or example code to sync the editors’ internal scroll positions for side-by-side text comparison?

Synchronizing vertical scroll between two Trumbowyg editors using Alpine.js is straightforward once you target the right elements—the .trumbowyg-editor contenteditable divs inside each instance, not the hidden textareas. Your current workaround scrolls the page because it’s likely hooking the outer containers; instead, sync the internal scrollTop positions directly with Alpine.js @scroll events or vanilla JavaScript for bidirectional control. This keeps toolbars pinned and visible via Trumbowyg’s fixedBtnPane logic, perfect for side-by-side text comparison without janky page jumps.


Contents


Trumbowyg Scroll Mechanics

Trumbowyg isn’t just a textarea wrapper—under the hood, it’s a full WYSIWYG with a hidden textarea feeding a .trumbowyg-editor div that’s actually scrollable. Why does this trip people up? Most folks target the textarea directly, but that does nothing for visual scrolling. The real action happens in that contenteditable div.

Check the official Trumbowyg documentation—it spells out the structure: toolbar up top (.trumbowyg-button-pane), editable area below (.trumbowyg-editor), and your original textarea stays hidden. Toolbars stay “fixed” thanks to scroll listeners that adjust padding on the box: e.$box.css({paddingTop: toolbarHeight}). Mess with outer scrolls, and boom—toolbars vanish or the page jumps, exactly like your workaround.

For sync scroll between two editors, grab refs to both .trumbowyg-editor elements. Set up listeners on their scroll events. When one fires, mirror scrollTop to the other. Simple? Yeah, but bidirectional means handling both directions without infinite loops—use flags or throttled events.

Ever compared long code diffs side-by-side? This setup shines there, keeping cursors and highlights aligned visually.


Alpine.js Setup for Sync Scroll

Alpine.js makes this declarative and reactive—no jQuery bloat needed. Start with x-data on a parent wrapper holding both editors. Inside, use $refs to access the scrollable divs post-initialization, since Trumbowyg inits them dynamically.

The Alpine.js docs on @scroll are gold: attach @scroll="handleScroll($event)" to each editor container. Browsers smooth-scroll natively, so you get buttery performance. But dots in event names? Use dashes and .dot modifier if needed—@scroll.dot="...".

Here’s the skeleton in your HTML:

html
<div x-data="editorSync()">
 <div x-ref="editor1" class="trumbowyg-editor" @scroll="syncToOther($event, $refs.editor2)"></div>
 <!-- Trumbowyg inits here, injecting the real .trumbowyg-editor -->
</div>

Alpine.js computed values can track scroll positions reactively too—handy if you want to persist or animate. But for raw sync, stick to event handlers. Throttle if content’s massive: scroll events fire like crazy.

Your page-scroll issue? That’s because Trumbowyg’s outer .trumbowyg-box might capture events. Isolate to the inner editor divs.


Vanilla JavaScript Sync Approach

Prefer no frameworks? Vanilla JS nails sync scroll textarea vibes reliably. Grab elements after Trumbowyg inits (use trumbowyg.init callbacks), then bind scroll listeners.

Borrow from classics like the jQuery sync example on DotNetCurry—it sets scrollTop bidirectionally:

javascript
const editor1 = document.querySelector('#editor1 .trumbowyg-editor');
const editor2 = document.querySelector('#editor2 .trumbowyg-editor');

const syncScroll = (source, target) => {
 target.scrollTop = source.scrollTop;
};

editor1.addEventListener('scroll', () => syncScroll(editor1, editor2));
editor2.addEventListener('scroll', () => syncScroll(editor2, editor1));

No loops—user scrolls one, the other follows instantly. Add requestAnimationFrame for smoothness on long content:

javascript
const rafSync = (source, target) => {
 requestAnimationFrame(() => target.scrollTop = source.scrollTop);
};

This mirrors Stack Overflow discussions where folks hit the same textarea-vs-editor snag. Pro tip: init after Trumbowyg loads, or use MutationObserver for dynamic divs.


Full Working Alpine.js Example

Let’s build it. Assume two Trumbowyg instances side-by-side. Alpine.js handles refs cleanly.

HTML setup:

html
<!DOCTYPE html>
<html x-data="editorSync()">
<head>
 <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
 <link rel="stylesheet" href="trumbowyg.css">
 <script src="trumbowyg.min.js"></script>
</head>
<body>
 <div class="editors-container">
 <div class="editor-wrapper">
 <textarea id="editor1"></textarea>
 </div>
 <div class="editor-wrapper">
 <textarea id="editor2"></textarea>
 </div>
 </div>

 <script>
 function editorSync() {
 return {
 editor1: null,
 editor2: null,
 init() {
 // Init Trumbowyg first
 $('#editor1').trumbowyg({fixedBtnPane: true});
 $('#editor2').trumbowyg({fixedBtnPane: true});
 
 // Grab scrollables after init
 this.editor1 = document.querySelector('#editor1').closest('.trumbowyg-box').querySelector('.trumbowyg-editor');
 this.editor2 = document.querySelector('#editor2').closest('.trumbowyg-box').querySelector('.trumbowyg-editor');
 
 this.editor1.addEventListener('scroll', () => this.syncScroll(this.editor1, this.editor2));
 this.editor2.addEventListener('scroll', () => this.syncScroll(this.editor2, this.editor1));
 },
 syncScroll(source, target) {
 target.scrollTop = source.scrollTop;
 }
 }
 }
 </script>
</body>
</html>

Boom—scroll one, both move. Alpine.js computed values? Add scrollPos: 0 and $watch('scrollPos', val => { this.editor2.scrollTop = val; }), but events suffice for most.

Tested this? Line up matching text; cursors stay synced. For diffs, highlight changes—scroll locks 'em tight.


Toolbar Visibility and Fixes

Toolbars hiding? Trumbowyg’s fixedBtnPane: true listens to box scrolls, not inner editor ones. Your page scroll workaround triggered outer events, fooling the padding calc.

Fix: Ensure listeners are strictly on .trumbowyg-editor. If toolbars still dip, force the padding refresh:

javascript
const refreshToolbar = (box) => {
 const toolbar = box.querySelector('.trumbowyg-button-pane');
 box.style.paddingTop = toolbar.offsetHeight + 'px';
};

Call after sync. Or disable fixed mode and position manually—position: sticky on toolbars works cross-browser now.

Edge cases? Mobile touch-scrolls lag? Throttle with lodash or native:

javascript
let ticking = false;
function throttledSync(source, target) {
 if (!ticking) {
 requestAnimationFrame(() => {
 target.scrollTop = source.scrollTop;
 ticking = false;
 });
 ticking = true;
 }
}

Sync scroll feels native. Resize windows? Add @resize.window in Alpine to rebind refs.


Advanced Library Option

Want zero boilerplate? Drop in syncscroll library—946 bytes, vanilla JS. Just add class="syncscroll" name="mygroup" to both .trumbowyg-editor divs post-init:

javascript
syncscroll({groups: [{name: 'mygroup'}]});

It handles multi-element sync out-of-box, even horizontal if needed. Like diff2html’s side-by-side, but pure scroll. Integrate with Alpine.js via x-init.

Overkill for two editors? Maybe. But scales if you add more panes. No perf hit—micro indeed.


Sources

  1. Stack Overflow: Sync scroll between 2 textarea editors
  2. GitHub: asvd/syncscroll
  3. .NET Curry: Synchronize Scrolling of Two Multiline TextBoxes
  4. Trumbowyg Documentation
  5. Alpine.js: on directive
  6. GitHub: rtfpessoa/diff2html

Conclusion

Syncing Trumbowyg editors with Alpine.js boils down to targeting .trumbowyg-editor scrollTop—ditch page hacks for internal events, and toolbars stay put. Grab the full example, tweak for your diffs, and you’ll have responsive side-by-side comparison in minutes. Vanilla works too; scale with syncscroll if needed. Questions on alpine js computed values for positions? It’s reactive magic waiting.

Authors
Verified by moderation
NeuroAnswers
Moderation
Alpine.js: Sync Trumbowyg Vertical Scrolling with Code