JSFiddle - React, Tailwind, and code Playground
HTML
<div class="timeline">
<div class="event">
<div class="event__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus varius sodales purus, id gravida ipsum accumsan quis. Donec ultrices orci quis ex consequat mollis. Etiam in gravida enim.
</div>
</div>
<div class="event">
<div class="event__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus varius sodales purus, id gravida ipsum accumsan quis. Donec ultrices orci quis ex consequat mollis. Etiam in gravida enim.
</div>
</div>
<div class="event">
<div class="event__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus varius sodales purus, id gravida ipsum accumsan quis. Donec ultrices orci quis ex consequat mollis. Etiam in gravida enim.
</div>
</div>
<div class="event">
<div class="event__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus varius sodales purus, id gravida ipsum accumsan quis. Donec ultrices orci quis ex consequat mollis. Etiam in gravida enim.
</div>
</div>
<div class="event">
<div class="event__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus varius sodales purus, id gravida ipsum accumsan quis. Donec ultrices orci quis ex consequat mollis. Etiam in gravida enim.
</div>
</div>
</div>
CSS
/* line in center */
.timeline {
position: relative;
width: 100%;
}
.timeline:before {
content: " ";
display: block;
position: absolute;
top: 0;
left: calc(50% - 2px);
width: 4px;
height: 100%;
background: red;
}
/* events */
.event {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 50%;
height: auto;
padding: 0 20px 0 0;
}
.event--right {
/* Move padding to other side.
* It is important that the padding does not change, because
* changing the height of an element (which padding can do)
* while layouting is not handled by the JavaScript. */
padding: 0 0 0 20px;
}
/* discs on timeline */
.event:after {
content: "";
display: block;
position: absolute;
top: calc(50% - 5px);
width: 0;
height: 0;
border-style: solid;
}
.event--left:after {
right: 0;
border-width: 10px 0 10px 20px;
border-color: transparent transparent transparent #de2d26;
}
.event--right:after {
left: 0;
border-width: 10px 20px 10px 0;
border-color: transparent #de2d26 transparent transparent;
}
/* event styling */
.event__body {
padding: 20px;
background: #de2d26;
color: rgba(255, 255, 255, 0.9);
}
JavaScript
// wrap in anonymous function to not pollute global namespace
(function() {
// find the minimium element in an array, and the index of it
function min(arr) {
var ind = -1, min = false, i;
for (i = 0; i < arr.length; ++i) {
if (min === false || arr[i] < min) {
ind = i;
min = arr[i];
}
}
return {
index: ind,
value: min
};
}
// position events within a single timeline container
function updateTimeline(timeline, minSpace = 10) {
var events = Array.from(timeline.querySelectorAll('.event')),
tops = [0, 0],
bottoms = [0, 0],
minTops = [0, 0];
// determine height of container, upper bound
timeline.style.height = events.reduce(function(sum, event) {
return sum + event.offsetHeight;
}, 0) + 'px';
// position events in timeline
events.forEach(function(event) {
// find highest point to put event at
// first put it with its top aligned to the lowest bottom - this will
// always yield a good solution, we check better ones later
var h = event.offsetHeight,
y = min(bottoms),
x = (y.index === 0 ? 0 : 50),
b = y.value + h,
m = (tops[1 - y.index] + bottoms[1 - y.index]) / 2;
event.className = 'event';
event.classList.add('event--' + (y.index === 0 ? 'left' : 'right'));
// try to squeeze element up as high as possible
// first try to put midpoint of new event just below the midpoint of
// the last placed event on the other side
if (m + minSpace - h / 2 > minTops[y.index]) {
y.value = m + minSpace - h / 2;
b = y.value + h;
}
// it would be even better if the top of the new event could move
// all the way up - this can be done if its midpoint is below the
// midpoint of the last event on the other side
if (minTops[y.index] + h / 2 > m + minSpace) {
y.value = minTops[y.index];
b = y.value + h;
...