Hi I am trying to create a timeline for my fantasy world, and it works for the most part. You can see it in action here. https://www.skazkaworld.com/timeline.html
As you can see, the background rune image from the 8th entry and beyond does not display correctly. I cannot for the life of me figure out what the issue is. I'm still learning so my code is not very elegant.
Relevant HTML:
<body>
<div id="navbar"></div><!-- Navbar will be loaded here -->
<div class="timeline" id="timeline"></div>
<script src="main.js"></script>
<script src="timeline.js"></script>
<script>
// Load navbar.html dynamically
fetch("navbar.html")
.then(response => response.text())
.then(data => {
document.getElementById("navbar").innerHTML = data;
// Now that navbar is loaded, attach hamburger event
const hamburger = document.querySelector(".hamburger");
const navLinks = document.querySelector(".nav-links");
if (hamburger && navLinks) {
hamburger.addEventListener("click", () => {
navLinks.classList.toggle("active");
hamburger.classList.toggle("open");
});
}
});
</script>
</body>
CSS:
/* Timeline */ .timeline {
display: block;
margin: 100px auto 20px;
width: 800px;
padding: 14px;
background: rgba(20, 20, 20, 0.85);
height: 80vh;
overflow: auto;
background-image: url("assets/timeline.jpg"); /* replace with your image */
background-repeat: repeat-y; /* repeats vertically */
background-position: center top; /* centers it horizontally */
background-size:cover; /* adjust if needed, e.g., "contain" or specific width */
}
.event {
margin: 18px 0;
cursor: pointer;
} .event-header {
display: flex;
align-items: center;
}
.rune {
flex-shrink: 0;
width: 28px;
height: 28px;
margin-right: 8px;
background: url("assets/rune-marker.png") center/contain no-repeat;
filter: grayscale(100%) brightness(0.9);
transition: filter 0.3s ease, transform 0.35s ease;
}
.rune.active { filter: grayscale(0%) brightness(1.1);
transform: scale(1.12); }
.event-details {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.4s ease, opacity 0.4s ease;
padding-left: 36px;
font-size: 0.9em;
color: #ccc;
}
.event.open .event-details {
max-height: 200px;
opacity: 1;
margin-top: 4px;
}
Finally the JS:
// timeline.js
document.addEventListener("DOMContentLoaded", () => {
const timeline = document.getElementById("timeline");
// Timeline data array
const timelineEvents = [
{
year: "1200 AE",
title: "Founding of the First Ascent",
details: "The First Ascent arises atop the frozen peaks of Skazka, where mortals defied the giants."
},
{
year: "1345 AE",
title: "The Rift of Molach",
details: "A tear in the weave of reality as Molach's fall tore open the land and spirits slipped through."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
{
year: "1502 AE",
title: "The Night of Whispering",
details: "When every shadow spoke and the forests sang curses in voices not their own."
},
// Add more events here...
];
// Generate events dynamically
timelineEvents.forEach((ev, i) => {
const eventDiv = document.createElement("div");
eventDiv.className = "event";
eventDiv.dataset.id = i;
eventDiv.innerHTML = `
<div class="event-header">
<span class="rune"></span>
<div>
<strong>${ev.year}</strong><br>
${ev.title}
</div>
</div>
<div class="event-details">
${ev.details}
</div>
`;
timeline.appendChild(eventDiv);
});
// Scroll rune activation - FIXED to use timeline container scroll
const events = document.querySelectorAll(".event");
function onTimelineScroll() {
const timelineRect = timeline.getBoundingClientRect();
const scrollTop = timeline.scrollTop;
const viewportMiddle = timelineRect.height / 2;
events.forEach(evt => {
const eventRect = evt.getBoundingClientRect();
const timelineTop = timelineRect.top;
const eventTop = eventRect.top - timelineTop + scrollTop;
const rune = evt.querySelector(".rune");
// Activate rune if event is in upper half of timeline viewport
if (eventTop < scrollTop + viewportMiddle) {
rune.classList.add("active");
} else {
rune.classList.remove("active");
}
});
}
// Toggle event details on click
events.forEach(evt => {
evt.addEventListener("click", () => {
evt.classList.toggle("open");
});
});
// Listen to timeline scroll instead of window scroll
timeline.addEventListener("scroll", onTimelineScroll);
onTimelineScroll(); // Initial call
});
If what you mean by display incorrectly is that the markers are gray, that's what your code is doing by design:
// Activate rune if event is in upper half of timeline viewport
if (eventTop < scrollTop + viewportMiddle) {
rune.classList.add("active");
} else {
rune.classList.remove("active");
}
You adding active
for the elements in the upper half but removing it for the lower half. According to your stylesheet only with active
will the marker be blue