JSFiddle - React, Tailwind, and code Playground
HTML
<div class="wrapper">
<ul class="inner">
<li>first</li>
<li>second</li>
<li>third</li>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
<li>I</li>
<li>J</li>
<li>K</li>
<li>L</li>
<li>M</li>
<li>N</li>
<li>O</li>
<li>P</li>
<li>Q</li>
<li>last</li>
</ul>
<div class="spacer"></div>
</div>
<button id="add">Add To Top</button>
<button id="remove">Remove From Top</button>
CSS
.wrapper {
background-color: #FFE;
height: 200px;
overflow-y: scroll;
}
.inner {
height: 200px;
margin: 0;
position: sticky;
top: 0;
left: 0;
right: 0;
transform: scaleY(-1);
overflow: hidden;
border-left: dashed green 4px;
}
.inner > * {
transform: scaleY(-1);
}
.spacer {
border-right: dashed red 4px;
}
JavaScript
const spacer = document.querySelector('.spacer');
const inner = document.querySelector('.inner');
const wrapper = document.querySelector('.wrapper');
function syncWrapper() {
// scrolls & resizes the scrollbar to match the inner element view
spacer.style.height = (inner.scrollHeight - inner.offsetHeight) + 'px';
const fromTop = inner.scrollHeight - inner.scrollTop - inner.offsetHeight;
wrapper.scrollTop = fromTop;
}
function syncInner(e) {
// scrolls the inner element view to match the outer scrollbar
const fromBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.offsetHeight;
inner.scrollTop = fromBottom;
}
syncWrapper();
wrapper.addEventListener('scroll', syncInner, { passive: true });
document.getElementById('add').addEventListener('click', e => {
const item = document.createElement('li');
item.appendChild(document.createTextNode('bonus content'));
inner.appendChild(item);
syncWrapper();
});
document.getElementById('remove').addEventListener('click', e => {
inner.removeChild(inner.querySelector('li:last-child'));
syncWrapper();
});