Create Horizontal Timeline using HTML, CSS, and JavaScript

Faraz

By Faraz -

Learn how to create a responsive horizontal timeline using HTML, CSS, and JavaScript with step-by-step instructions. Perfect for portfolios and websites.


create-horizontal-timeline-using-html-css-javascript.webp

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Conclusion
  6. Preview

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.

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:

create-horizontal-timeline-using-html-css-javascript.gif

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 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺