Learn how to create a responsive horizontal timeline using HTML, CSS, and JavaScript with step-by-step instructions. Perfect for portfolios and websites.
Table of Contents
Do you want to show your journey, project milestones, or company growth in a stylish way? A horizontal timeline is a great way to do it! With a simple combination of HTML, CSS, and JavaScript, you can create a beautiful timeline that scrolls horizontally and looks great on all devices.
This guide is beginner-friendly and uses simple words to help you learn quickly and rank higher on search engines.
Prerequisites
Before starting, you should have:
- Basic knowledge of HTML and CSS
- Basic understanding of JavaScript
- A text editor like VS Code
- A browser like Chrome
Source Code
Step 1 (HTML Code):
To get started, we first need to create a basic HTML file. In this file, we will include the main structure for our horizontal timeline.
<!DOCTYPE html>
<!DOCTYPE html>
- Declares the document type and version of HTML (HTML5).
- Ensures consistent rendering across web browsers.
<html lang="en">
<html lang="en">
- Opens the HTML document.
- Sets the language of the content to English.
<head>...</head>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Scroll Timeline</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
<link rel="stylesheet" href="https://unpkg.com/lucide@latest/dist/lucide.min.css">
<link rel="stylesheet" href="styles.css">
</head>
<meta charset="UTF-8">: Sets character encoding to UTF-8 (supports most characters).<meta name="viewport"...>: Makes the layout responsive on mobile devices.<title>: Sets the browser tab title.<link rel="stylesheet" href="...">:- Loads Google Fonts (Inter font family).
- Loads Lucide icon library.
- Loads a custom CSS file (styles.css).
<body>...</body>
<body>
...
</body>
- Contains all visible content and interactive elements of the webpage.
<div class="container">
<div class="container">
- A wrapper for the entire timeline content.
<div class="timeline-header">
<div class="timeline-header">
<h1>Our Journey</h1>
<p>Scroll to explore our milestones and achievements over the years</p>
</div>
- Displays the heading "Our Journey".
- Provides a short introductory paragraph.
<div class="timeline-wrapper">
<div class="timeline-wrapper">
- Container for the timeline line, progress animation, and individual timeline items.
Timeline Line and Progress Elements
<div class="timeline-line"></div>
<div class="timeline-progress"></div>
- .timeline-line: Static vertical or horizontal line representing the full timeline.
- .timeline-progress: Visual indicator of scroll progress on the timeline.
Timeline Items Container
<div class="timeline-items" id="timelineItems">
- Holds all the timeline events.
- ID timelineItems might be used in JavaScript to control scroll behavior or animations.
Individual Timeline Item Example
<div class="timeline-item">
<div class="timeline-dot" data-tooltip="Founded"></div>
<div class="timeline-date">2015</div>
<div class="timeline-card">
<i data-lucide="rocket"></i>
<h3>Company Founded</h3>
<p>Launched with a vision to revolutionize the industry with innovative solutions and cutting-edge technology.</p>
<a href="#" class="btn">Learn More</a>
</div>
</div>
- .timeline-item: A single milestone.
- .timeline-dot: Visual point on the timeline; includes a data-tooltip for hover hints.
- .timeline-date: Shows the year (e.g., 2015).
- .timeline-card: Contains:
- Icon (
<i data-lucide="rocket">) from Lucide library. - Title and description of the milestone.
- A button link for more details.
- Icon (
This structure repeats for different years: 2016 (Product Launch), 2017 (Series A), etc.
Timeline Navigation
<div class="timeline-nav" id="timelineNav"></div>
- Placeholder for scroll navigation buttons or indicators.
Scripts at the Bottom
<script src="https://unpkg.com/lucide@latest"></script>
<script src="script.js"></script>
- Loads Lucide's icon rendering JavaScript.
- Loads your custom JavaScript file (script.js) for interactions like scroll-based animations, timeline navigation, or dynamic effects.
Step 2 (CSS Code):
Once the basic HTML structure of the timeline is in place, the next step is to add styling to the timeline using CSS. Let's break down the CSS code step by step:
1. Root Variables
:root {
--primary: #6366f1;
--primary-light: #a5b4fc;
--secondary: #f59e0b;
--text: #1e293b;
--text-light: #64748b;
--bg: #f8fafc;
--card-bg: #ffffff;
}
- Defines CSS variables for consistent color use.
- --primary and --secondary: Main accent colors.
- --text, --text-light: Used for font colors.
- --bg, --card-bg: Background colors for page and cards.
2. Global Reset
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
- Removes default browser spacing.
- Uses border-box for predictable width/height.
3. Body Styling
body {
font-family: 'Inter', sans-serif;
color: var(--text);
background: var(--bg);
overflow-x: hidden;
height: 100vh;
}
- Applies the Inter font.
- Sets base text and background color.
- Hides horizontal scroll and uses full viewport height.
4. Container
.container {
max-width: 100%;
padding: 1rem 0;
height: 100%;
}
- Full width layout with top-bottom padding.
- Uses full height for the scroll section.
5. Timeline Header
.timeline-header {
text-align: center;
margin-bottom: 1rem;
padding: 0 2rem;
}
- Centers text, adds spacing, and horizontal padding.
.timeline-header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
- Big bold heading with gradient text using clipping.
.timeline-header p {
font-size: 1.1rem;
color: var(--text-light);
max-width: 600px;
margin: 0 auto;
}
- Paragraph under the heading with a softer text color.
6. Timeline Wrapper
.timeline-wrapper {
position: relative;
padding: 2rem 0;
height: calc(100% - 150px);
}
- Positioned container for timeline layout.
- Takes nearly full screen height, minus header.
7. Timeline Line and Progress
.timeline-line {
position: absolute;
top: 57%;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform: translateY(-50%);
z-index: 1;
border-radius: 2px;
box-shadow: 0 0 10px rgba(99, 102, 241, 0.3);
}
- Main horizontal timeline line with gradient and glow.
.timeline-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--primary), var(--secondary));
border-radius: 2px;
z-index: 2;
transition: width 0.3s ease-out;
}
- Animated progress indicator that fills as you scroll.
8. Timeline Items Container
.timeline-items {
display: flex;
padding: 0 10%;
scroll-snap-type: x mandatory;
overflow-x: scroll;
scroll-behavior: smooth;
position: relative;
z-index: 3;
padding-top: 6rem;
padding-bottom: 2rem;
-ms-overflow-style: none;
scrollbar-width: none;
height: 100%;
align-items: center;
}
- Horizontal scroll layout for cards.
- Uses scroll-snap for smooth snapping.
- Hides scrollbars and centers items.
.timeline-items::-webkit-scrollbar {
display: none;
}
- Hides the scrollbar on WebKit browsers (Chrome/Safari).
9. Timeline Item Styling
.timeline-item {
flex: 0 0 400px;
scroll-snap-align: center;
margin: 0 2rem;
position: relative;
opacity: 0.3;
transform: translateY(20px);
transition: all 0.5s ease;
}
- Each item is 400px wide, centered in a snap scroll.
- Starts with lower opacity and downward shift for animation.
.timeline-item.active {
opacity: 1;
transform: translateY(0);
}
- When active (in view), it becomes fully visible.
10. Timeline Card
.timeline-card {
position: relative;
padding: 2rem;
border-radius: 1rem;
background:
linear-gradient(var(--card-bg), var(--card-bg)) padding-box,
linear-gradient(135deg, var(--primary), var(--secondary)) border-box;
border: 2px solid transparent;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
- Card with rounded corners, gradient border using padding-box, and shadow.
.timeline-card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
- Lift-up effect on hover for interaction feedback.
11. Timeline Date & Dot
.timeline-date {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 0.5rem 1.5rem;
border-radius: 2rem;
font-weight: 600;
font-size: 0.9rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
z-index: 4;
}
- Shows the year above each card.
- Styled with gradient, shadow, and rounded corners.
.timeline-dot {
position: absolute;
top: -3rem;
left: 50%;
transform: translateX(-50%);
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--primary);
border: 4px solid var(--bg);
z-index: 5;
transition: all 0.3s ease;
}
- The small circle (dot) marking the point in time.
.timeline-item.active .timeline-dot {
transform: translateX(-50%) scale(1.2);
box-shadow: 0 0 0 6px rgba(99, 102, 241, 0.2);
animation: pulse 2s infinite;
}
- Enlarges and adds a glow animation for the active dot.
12. Card Content
.timeline-card i {
font-size: 1.5rem;
margin-bottom: 1rem;
display: inline-block;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
- Icon with gradient text effect.
.timeline-card h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
- Card title styling.
.timeline-card p {
color: var(--text-light);
line-height: 1.6;
margin-bottom: 1rem;
}
- Paragraph with readable spacing and lighter color.
.timeline-card .btn {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.timeline-card .btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3),
0 4px 6px -2px rgba(99, 102, 241, 0.1);
}
- Button with gradient and lift-on-hover effect.
13. Navigation Dots
.timeline-nav {
display: flex;
justify-content: center;
margin-top: 3rem;
gap: 1rem;
}
- Layout for navigation dots under timeline.
.timeline-nav-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-light);
cursor: pointer;
transition: all 0.3s ease;
}
.timeline-nav-dot.active {
background: var(--primary);
transform: scale(1.3);
}
- Small circle dots indicating scroll position.
- Enlarged when active.
14. Pulse Animation
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
}
}
- Animates a glowing pulse around active timeline dots.
15. Responsive Design
@media (max-width: 768px) {
...
}
- Makes timeline vertical on smaller screens.
- Adjusts layout, scroll, and positions accordingly.
16. Tooltip for Timeline Dot
.timeline-dot::after {
content: attr(data-tooltip);
position: absolute;
...
}
- Shows tooltip text on hover.
- Tooltip styling changes for small screens too.
:root {
--primary: #6366f1;
--primary-light: #a5b4fc;
--secondary: #f59e0b;
--text: #1e293b;
--text-light: #64748b;
--bg: #f8fafc;
--card-bg: #ffffff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
color: var(--text);
background: var(--bg);
overflow-x: hidden;
height: 100vh;
}
.container {
max-width: 100%;
padding: 1rem 0;
height: 100%;
}
.timeline-header {
text-align: center;
margin-bottom: 1rem;
padding: 0 2rem;
}
.timeline-header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.timeline-header p {
font-size: 1.1rem;
color: var(--text-light);
max-width: 600px;
margin: 0 auto;
}
.timeline-wrapper {
position: relative;
padding: 2rem 0;
height: calc(100% - 150px);
}
.timeline-line {
position: absolute;
top: 57%;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform: translateY(-50%);
z-index: 1;
border-radius: 2px;
box-shadow: 0 0 10px rgba(99, 102, 241, 0.3);
}
.timeline-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--primary), var(--secondary));
border-radius: 2px;
z-index: 2;
transition: width 0.3s ease-out;
}
.timeline-items {
display: flex;
padding: 0 10%;
scroll-snap-type: x mandatory;
overflow-x: scroll;
scroll-behavior: smooth;
position: relative;
z-index: 3;
padding-top: 6rem;
padding-bottom: 2rem;
-ms-overflow-style: none;
scrollbar-width: none;
height: 100%;
align-items: center;
}
.timeline-items::-webkit-scrollbar {
display: none;
}
.timeline-item {
flex: 0 0 400px;
scroll-snap-align: center;
margin: 0 2rem;
position: relative;
opacity: 0.3;
transform: translateY(20px);
transition: all 0.5s ease;
}
.timeline-item.active {
opacity: 1;
transform: translateY(0);
}
.timeline-card {
position: relative;
padding: 2rem;
border-radius: 1rem;
background:
linear-gradient(var(--card-bg), var(--card-bg)) padding-box,
linear-gradient(135deg, var(--primary), var(--secondary)) border-box;
border: 2px solid transparent;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.timeline-card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.timeline-date {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 0.5rem 1.5rem;
border-radius: 2rem;
font-weight: 600;
font-size: 0.9rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
z-index: 4;
}
.timeline-dot {
position: absolute;
top: -3rem;
left: 50%;
transform: translateX(-50%);
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--primary);
border: 4px solid var(--bg);
z-index: 5;
transition: all 0.3s ease;
}
.timeline-item.active .timeline-dot {
transform: translateX(-50%) scale(1.2);
box-shadow: 0 0 0 6px rgba(99, 102, 241, 0.2);
animation: pulse 2s infinite;
}
.timeline-card i {
font-size: 1.5rem;
margin-bottom: 1rem;
display: inline-block;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.timeline-card h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.timeline-card p {
color: var(--text-light);
line-height: 1.6;
margin-bottom: 1rem;
}
.timeline-card .btn {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.timeline-card .btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3),
0 4px 6px -2px rgba(99, 102, 241, 0.1);
}
.timeline-nav {
display: flex;
justify-content: center;
margin-top:3rem;
gap: 1rem;
}
.timeline-nav-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-light);
cursor: pointer;
transition: all 0.3s ease;
}
.timeline-nav-dot.active {
background: var(--primary);
transform: scale(1.3);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
}
}
/* Responsive styles */
@media (max-width: 768px) {
.timeline-items {
flex-direction: column;
padding: 0 2rem;
overflow-x: visible;
gap: 4rem;
align-items: flex-start;
}
.timeline-item {
flex: 1;
margin: 0;
opacity: 1;
transform: none;
width: 100%;
}
.timeline-line {
left: 2rem;
top: 0;
bottom: 0;
width: 4px;
height: auto;
transform: none;
}
.timeline-progress {
width: 4px;
height: 0;
transition: height 0.3s ease-out;
}
.timeline-dot {
top: -2rem;
left: 1.8rem;
transform: translateX(0);
}
.timeline-item.active .timeline-dot {
transform: scale(1.2);
}
.timeline-date {
left: 4rem;
transform: none;
}
.timeline-card {
margin-left: 4rem;
}
}
/* Tooltip styles */
.timeline-dot::after {
content: attr(data-tooltip);
position: absolute;
top: -3rem;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
}
.timeline-dot:hover::after {
opacity: 1;
top: -4.5rem;
}
@media (max-width: 768px) {
.timeline-dot::after {
left: 3rem;
top: -1.5rem;
}
.timeline-dot:hover::after {
left: 3rem;
top: -3rem;
}
} Step 3 (JavaScript Code):
Finally, we need to create a function in JavaScript. Let's break down the JavaScript code step by step:
1. DOM Ready Event
document.addEventListener('DOMContentLoaded', () => {
- Ensures all HTML is fully loaded before running the script.
2. Initialize Lucide Icons
lucide.createIcons();
- Converts
<i data-lucide="...">elements into SVG icons using the Lucide library.
3. Select Important Elements
const timelineItems = document.getElementById('timelineItems');
const timelineProgress = document.querySelector('.timeline-progress');
const timelineNav = document.getElementById('timelineNav');
const items = document.querySelectorAll('.timeline-item');
- Gets references to key DOM elements:
- The horizontal scroll container (#timelineItems)
- The scroll progress bar
- The navigation dot container
- All timeline items (cards)
4. Create Navigation Dots
items.forEach((item, index) => {
const dot = document.createElement('div');
dot.classList.add('timeline-nav-dot');
dot.dataset.index = index;
timelineNav.appendChild(dot);
dot.addEventListener('click', () => {
scrollToItem(index);
});
});
- For each .timeline-item:
- Creates a dot and adds it to .timeline-nav.
- On click, scrolls smoothly to the corresponding item.
5. Setup IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
const index = Array.from(items).indexOf(entry.target);
document.querySelectorAll('.timeline-nav-dot').forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});
}
});
}, {
threshold: 0.5,
});
- Observes each timeline item.
- When an item is 50% visible, it:
- Adds .active class (for animation).
- Highlights the corresponding navigation dot.
6. Observe All Timeline Items
items.forEach((item) => {
observer.observe(item);
});
- Activates the observer on each timeline card.
7. Scroll Progress Update
timelineItems.addEventListener('scroll', () => {
updateProgress();
});
- Updates the progress bar when the user scrolls.
function updateProgress() {
const scrollWidth = timelineItems.scrollWidth - timelineItems.clientWidth;
const scrollPosition = timelineItems.scrollLeft;
const progress = (scrollPosition / scrollWidth) * 100;
if (window.innerWidth > 768) {
timelineProgress.style.width = `${progress}%`;
} else {
const itemHeight = items[0].offsetHeight + 64;
const scrollHeight = timelineItems.scrollHeight - timelineItems.clientHeight;
const verticalProgress = (timelineItems.scrollTop / scrollHeight) * 100;
timelineProgress.style.height = `${verticalProgress}%`;
}
}
- Calculates scroll progress in horizontal (desktop) or vertical (mobile) mode.
- Updates the progress bar’s width or height accordingly.
8. Scroll to Item on Dot Click
function scrollToItem(index) {
const item = items[index];
if (window.innerWidth > 768) {
timelineItems.scrollTo({
left: item.offsetLeft - timelineItems.clientWidth / 2 + item.clientWidth / 2,
behavior: 'smooth',
});
} else {
timelineItems.scrollTo({
top: item.offsetTop - 100,
behavior: 'smooth',
});
}
}
- Scrolls the container to center the selected item.
- Uses different scroll directions depending on screen width.
9. Activate First Item on Load
if (items.length > 0) {
items[0].classList.add('active');
document.querySelector('.timeline-nav-dot').classList.add('active');
}
- Sets the first card and dot as active when the page loads.
10. Update Progress on Window Resize
window.addEventListener('resize', () => {
updateProgress();
});
- Ensures the progress bar adjusts if the screen size changes.
11. Mouse Wheel Horizontal Scroll (Desktop)
timelineItems.addEventListener('wheel', (e) => {
if (window.innerWidth <= 768) return;
e.preventDefault();
if (isScrolling) return;
...
}, { passive: false });
- Prevents default vertical scroll behavior.
- Uses the wheel event to scroll horizontally instead.
- Calculates direction and scrolls one card-width left or right.
12. Scroll Debounce for Smoothness
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
isScrolling = false;
}, 800);
- Prevents multiple scrolls from triggering quickly.
- Waits 800ms before allowing the next scroll.
13. Touch Event Handling (for Swipe)
timelineItems.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
timelineItems.addEventListener('touchend', (e) => {
if (window.innerWidth <= 768) return;
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, { passive: true });
- Detects swipe direction on touchstart and touchend events.
- Only works on desktop layout (> 768px).
14. Swipe Handling Logic
function handleSwipe() {
...
}
- Detects swipe distance and direction.
- If user swiped enough distance:
- Scrolls left or right by one card.
- Uses smooth scrolling.
document.addEventListener('DOMContentLoaded', () => {
// Initialize Lucide icons
lucide.createIcons();
const timelineItems = document.getElementById('timelineItems');
const timelineProgress = document.querySelector('.timeline-progress');
const timelineNav = document.getElementById('timelineNav');
const items = document.querySelectorAll('.timeline-item');
// Create navigation dots
items.forEach((item, index) => {
const dot = document.createElement('div');
dot.classList.add('timeline-nav-dot');
dot.dataset.index = index;
timelineNav.appendChild(dot);
dot.addEventListener('click', () => {
scrollToItem(index);
});
});
// Set up IntersectionObserver for animations
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
// Update active nav dot
const index = Array.from(items).indexOf(entry.target);
document.querySelectorAll('.timeline-nav-dot').forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});
}
});
},
{
threshold: 0.5,
}
);
items.forEach((item) => {
observer.observe(item);
});
// Update progress bar on scroll
timelineItems.addEventListener('scroll', () => {
updateProgress();
});
function updateProgress() {
const scrollWidth = timelineItems.scrollWidth - timelineItems.clientWidth;
const scrollPosition = timelineItems.scrollLeft;
const progress = (scrollPosition / scrollWidth) * 100;
if (window.innerWidth > 768) {
timelineProgress.style.width = `${progress}%`;
} else {
const itemHeight = items[0].offsetHeight + 64;
const scrollHeight =
timelineItems.scrollHeight - timelineItems.clientHeight;
const verticalProgress = (timelineItems.scrollTop / scrollHeight) * 100;
timelineProgress.style.height = `${verticalProgress}%`;
}
}
function scrollToItem(index) {
const item = items[index];
if (window.innerWidth > 768) {
timelineItems.scrollTo({
left:
item.offsetLeft -
timelineItems.clientWidth / 2 +
item.clientWidth / 2,
behavior: 'smooth',
});
} else {
timelineItems.scrollTo({
top: item.offsetTop - 100,
behavior: 'smooth',
});
}
}
// Initialize first item as active
if (items.length > 0) {
items[0].classList.add('active');
document.querySelector('.timeline-nav-dot').classList.add('active');
}
// Handle window resize
window.addEventListener('resize', () => {
updateProgress();
});
// Mouse wheel horizontal scrolling
let isScrolling = false;
let scrollEndTimer;
timelineItems.addEventListener(
'wheel',
(e) => {
if (window.innerWidth <= 768) return; // Only apply on desktop
e.preventDefault();
if (isScrolling) return;
isScrolling = true;
// Determine scroll direction
const delta = Math.sign(e.deltaY);
const currentScroll = timelineItems.scrollLeft;
const itemWidth = items[0].offsetWidth + 64; // Including margin
let targetScroll;
if (delta > 0) {
// Scroll right
targetScroll = Math.min(
currentScroll + itemWidth,
timelineItems.scrollWidth - timelineItems.clientWidth
);
} else {
// Scroll left
targetScroll = Math.max(currentScroll - itemWidth, 0);
}
// Smooth scroll to target position
timelineItems.scrollTo({
left: targetScroll,
behavior: 'smooth',
});
// Reset scrolling flag after animation completes
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
isScrolling = false;
}, 800); // Match this with your CSS transition duration
},
{ passive: false }
);
// Touch events for mobile
let touchStartX = 0;
let touchEndX = 0;
timelineItems.addEventListener(
'touchstart',
(e) => {
touchStartX = e.changedTouches[0].screenX;
},
{ passive: true }
);
timelineItems.addEventListener(
'touchend',
(e) => {
if (window.innerWidth <= 768) return; // Only apply on desktop
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
},
{ passive: true }
);
function handleSwipe() {
if (isScrolling) return;
const threshold = 50; // Minimum swipe distance
const deltaX = touchEndX - touchStartX;
if (Math.abs(deltaX) < threshold) return;
isScrolling = true;
const currentScroll = timelineItems.scrollLeft;
const itemWidth = items[0].offsetWidth + 64; // Including margin
let targetScroll;
if (deltaX > 0) {
// Swipe right - scroll left
targetScroll = Math.max(currentScroll - itemWidth, 0);
} else {
// Swipe left - scroll right
targetScroll = Math.min(
currentScroll + itemWidth,
timelineItems.scrollWidth - timelineItems.clientWidth
);
}
timelineItems.scrollTo({
left: targetScroll,
behavior: 'smooth',
});
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
isScrolling = false;
}, 800);
}
});Final Output:
Conclusion:
That’s it! You’ve now built a responsive horizontal timeline using HTML, CSS, and JavaScript. It’s perfect for showcasing project journeys, company growth, or personal achievements in a modern and engaging way.
This layout is mobile-friendly, smooth to scroll, and easy to understand, making it perfect for portfolios or startup websites. Feel free to customize the colors, fonts, or animations to match your brand.
That’s a wrap!
I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.
Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee
And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!
Thanks!
Faraz 😊


