Show timeline items with fade-in effect

by der_robert

HTML

<div class="timeline">
  <div class="timeline-item">
    <div class="timeline-content">
      <span class="timeline-icon">📝</span>
      <div class="log-header">
        <div class="log-date">05.08.2025 09:43</div>
        <div class="log-user">von <i>admin</i></div>
        <div class="log-description">Benutzerdaten aktualisiert</div>
      </div>
      <table class="log-changes">
        <thead>
          <tr>
            <th>Feld</th>
            <th>Vorher</th>
            <th>Nachher</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Name</td>
            <td>Max</td>
            <td>Maximilian</td>
          </tr>
          <tr>
            <td>PLZ</td>
            <td>12345</td>
            <td>54321</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>

  <div class="timeline-item">
    <div class="timeline-content">
      <span class="timeline-icon">đź”’</span>
      <div class="log-header">
        <div class="log-date">05.08.2025 10:15</div>
        <div class="log-user">von <i>hannah</i></div>
        <div class="log-description">Passwort zurĂĽckgesetzt</div>
      </div>
      <table class="log-changes">
        <thead>
          <tr>
            <th>Feld</th>
            <th>Vorher</th>
            <th>Nachher</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Passwort</td>
            <td>••••••</td>
            <td>••••••</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

SCSS

.timeline {
  position: relative;
  max-width: 900px;
  margin: 60px auto;
  padding: 0 20px;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 40px;
    width: 4px;
    background: #ccc;
  }

  .timeline-item {
    position: relative;
    margin-bottom: 50px;
    padding-left: 80px;

    .timeline-content {
      background: #fff;
      border-left: 4px solid #2196f3;
      padding: 20px 25px;
      border-radius: 10px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
      position: relative;

      .timeline-icon {
        position: absolute;
        left: -50px;
        top: 20px;
        width: 32px;
        height: 32px;
        background: #2196f3;
        color: white;
        border-radius: 50%;
        font-size: 18px;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 0 0 4px #e3f2fd;
      }

      .log-header {
        margin-bottom: 10px;

        .log-date {
          font-weight: bold;
          color: #333;
        }

        .log-user {
          font-style: italic;
          color: #666;
        }

        .log-description {
          margin-top: 5px;
          color: #444;
        }
      }

      .log-changes {
        width: 100%;
        border-collapse: collapse;
        margin-top: 15px;

        th, td {
          padding: 8px 10px;
          border: 1px solid #ddd;
        }

        th {
          background: #f3f3f3;
          color: #222;
          text-align: left;
        }

        td {
          background: #fcfcfc;
          word-break: break-word;
        }
      }
    }
  }

  @media screen and (max-width: 600px) {
    &::before {
      left: 20px;
    }

    .timeline-item {
      padding-left: 60px;

      .timeline-content {
        .timeline-icon {
          left: -40px;
        }
      }
    }
  }
}

JavaScript

document.addEventListener('DOMContentLoaded', () => {
  const items = document.querySelectorAll('.timeline-item');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  }, { threshold: 0.2 });

  items.forEach(item => observer.observe(item));
});