So erstellen Sie einen animierten Countdown-Timer mit HTML, CSS und JavaScript

Avatar of Mateusz Rybczonek
Mateusz Rybczonek am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Haben Sie jemals einen Countdown-Timer für ein Projekt benötigt? Für so etwas ist es vielleicht natürlich, zu einem Plugin zu greifen, aber es ist tatsächlich viel einfacher, als Sie vielleicht denken, und erfordert nur das Trio aus HTML, CSS und JavaScript. Lassen Sie uns gemeinsam einen erstellen!

Darauf arbeiten wir hin

Hier sind einige Dinge, die der Timer tut und die wir in diesem Beitrag behandeln werden

  • Zeigt die verbleibende Anfangszeit an
  • Konvertiert den Zeitwert in das MM:SS Format
  • Berechnet die Differenz zwischen der verbleibenden Anfangszeit und der verstrichenen Zeit
  • Ändert die Farbe, wenn die verbleibende Zeit gegen Null läuft
  • Zeigt den Fortschritt der verbleibenden Zeit als animierten Ring an

OK, das wollen wir, also packen wir's an!

Schritt 1: Beginnen Sie mit dem grundlegenden Markup und den Stilen

Lassen Sie uns mit der Erstellung einer grundlegenden Vorlage für unseren Timer beginnen. Wir fügen eine svg mit einem circle-Element darin hinzu, um einen Timerring zu zeichnen, der die verstrichene Zeit anzeigt, und ein span-Element, um den Wert der verbleibenden Zeit anzuzeigen. Beachten Sie, dass wir den HTML-Code in JavaScript schreiben und ihn in das DOM injizieren, indem wir auf das Element #app abzielen. Sicher, wir könnten vieles davon in eine HTML-Datei verschieben, wenn das eher Ihr Ding ist.

document.getElementById("app").innerHTML = `
<div class="base-timer">
  <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    <g class="base-timer__circle">
      <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45" />
    </g>
  </svg>
  <span>
    <!-- Remaining time label -->
  </span>
</div>
`;

Jetzt, da wir etwas Markup zum Arbeiten haben, lassen Sie es uns ein wenig aufhübschen, damit wir eine gute visuelle Grundlage haben. Insbesondere werden wir

  • die Größe des Timers festlegen
  • den Füll- und Konturstrich des Kreis-Wrapper-Elements entfernen, damit wir die Form erhalten, aber die verstrichene Zeit durchscheinen lassen
  • die Breite und Farbe des Rings festlegen
/* Sets the containers height and width */
.base-timer {
  position: relative;
  height: 300px;
  width: 300px;
}

/* Removes SVG styling that would hide the time label */
.base-timer__circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.base-timer__path-elapsed {
  stroke-width: 7px;
  stroke: grey;
}

Wenn das erledigt ist, erhalten wir eine grundlegende Vorlage, die so aussieht.

Schritt 2: Einrichten des Zeit-Labels

Wie Sie wahrscheinlich bemerkt haben, enthält die Vorlage ein leeres <span>, das die verbleibende Zeit enthalten wird. Wir werden diesen Platz mit einem richtigen Wert füllen. Wir haben zuvor gesagt, dass die Zeit im MM:SS Format vorliegen wird. Dazu erstellen wir eine Methode namens formatTimeLeft

function formatTimeLeft(time) {
  // The largest round integer less than or equal to the result of time divided being by 60.
  const minutes = Math.floor(time / 60);
  
  // Seconds are the remainder of the time divided by 60 (modulus operator)
  let seconds = time % 60;
  
  // If the value of seconds is less than 10, then display seconds with a leading zero
  if (seconds < 10) {
    seconds = `0${seconds}`;
  }

  // The output in MM:SS format
  return `${minutes}:${seconds}`;
}

Dann werden wir unsere Methode in der Vorlage verwenden

document.getElementById("app").innerHTML = `
<div class="base-timer">
  <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    <g class="base-timer__circle">
      <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
    </g>
  </svg>
  <span id="base-timer-label" class="base-timer__label">
    ${formatTime(timeLeft)}
  </span>
</div>
`

Um den Wert innerhalb des Rings anzuzeigen, müssen wir unsere Stile etwas aktualisieren.

.base-timer__label {
  position: absolute;
  
  /* Size should match the parent container */
  width: 300px;
  height: 300px;
  
  /* Keep the label aligned to the top */
  top: 0;
  
  /* Create a flexible box that centers content vertically and horizontally */
  display: flex;
  align-items: center;
  justify-content: center;

  /* Sort of an arbitrary number; adjust to your liking */
  font-size: 48px;
}

OK, wir sind bereit, mit dem timeLeft-Wert zu spielen, aber der Wert existiert noch nicht. Lassen Sie ihn uns erstellen und den Anfangswert auf unser Zeitlimit setzen.

// Start with an initial value of 20 seconds
const TIME_LIMIT = 20;

// Initially, no time has passed, but this will count up
// and subtract from the TIME_LIMIT
let timePassed = 0;
let timeLeft = TIME_LIMIT;

Und wir sind einen Schritt näher.

Genau! Jetzt haben wir einen Timer, der bei 20 Sekunden beginnt... aber er zählt noch nicht. Bringen wir ihn zum Leben, damit er bis auf Null zählt.

Schritt 3: Herunterzählen

Denken wir darüber nach, was wir brauchen, um die Zeit herunterzuzählen. Im Moment haben wir einen timeLimit-Wert, der unsere Anfangszeit darstellt, und einen timePassed-Wert, der angibt, wie viel Zeit vergangen ist, sobald der Countdown beginnt.

Was wir tun müssen, ist, den Wert von timePassed pro Sekunde um eins zu erhöhen und den timeLeft-Wert basierend auf dem neuen timePassed-Wert neu zu berechnen. Dies können wir mit der setInterval-Funktion erreichen.

Lassen Sie uns eine Methode namens startTimer implementieren, die

  • das Zählerintervall festlegt
  • den Wert timePassed jede Sekunde inkrementiert
  • den neuen Wert von timeLeft neu berechnet
  • den Wert des Labels in der Vorlage aktualisiert

Wir müssen auch die Referenz auf dieses Intervallobjekt behalten, um es bei Bedarf zu löschen – deshalb erstellen wir eine timerInterval-Variable.

let timerInterval = null;

document.getElementById("app").innerHTML = `...`

function startTimer() {
  timerInterval = setInterval(() => {
    
    // The amount of time passed increments by one
    timePassed = timePassed += 1;
    timeLeft = TIME_LIMIT - timePassed;
    
    // The time left label is updated
    document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
  }, 1000);
}

Wir haben eine Methode, die den Timer startet, aber wir rufen sie nirgends auf. Lassen Sie uns unseren Timer sofort beim Laden starten.

document.getElementById("app").innerHTML = `...`
startTimer();

Das war's! Unser Timer zählt jetzt die Zeit herunter. Das ist zwar schön und gut, aber es wäre schöner, wenn wir dem Ring um das Zeit-Label etwas Farbe hinzufügen und die Farbe bei verschiedenen Zeitwerten ändern könnten.

Schritt 4: Den Timerring mit einem weiteren Ring überdecken

Um die verstrichene Zeit zu visualisieren, müssen wir unserem Ring eine zweite Ebene hinzufügen, die die Animation steuert. Wir stapeln im Wesentlichen einen neuen grünen Ring über dem ursprünglichen grauen Ring, sodass der grüne Ring im Laufe der Zeit animiert wird, um den grauen Ring freizulegen, wie eine Fortschrittsanzeige.

Fügen wir zunächst ein path-Element in unser SVG-Element ein.

document.getElementById("app").innerHTML = `
<div class="base-timer">
  <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    <g class="base-timer__circle">
      <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
      <path
        id="base-timer-path-remaining"
        stroke-dasharray="283"
        class="base-timer__path-remaining ${remainingPathColor}"
        d="
          M 50, 50
          m -45, 0
          a 45,45 0 1,0 90,0
          a 45,45 0 1,0 -90,0
        "
      ></path>
    </g>
  </svg>
  <span id="base-timer-label" class="base-timer__label">
    ${formatTime(timeLeft)}
  </span>
</div>
`;

Als Nächstes erstellen wir eine Anfangsfarbe für den Pfad der verbleibenden Zeit.

const COLOR_CODES = {
  info: {
    color: "green"
  }
};

let remainingPathColor = COLOR_CODES.info.color;

Schließlich fügen wir ein paar Stile hinzu, damit der kreisförmige Pfad wie unser ursprünglicher grauer Ring aussieht. Wichtig ist hier, dass die stroke-width die gleiche Größe wie der ursprüngliche Ring hat und die Dauer der transition auf eine Sekunde eingestellt ist, damit sie reibungslos animiert wird und mit der verbleibenden Zeit im Zeit-Label übereinstimmt.

.base-timer__path-remaining {
  /* Just as thick as the original ring */
  stroke-width: 7px;

  /* Rounds the line endings to create a seamless circle */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Allows the ring to change color when the color value updates */
  stroke: currentColor;
}

.base-timer__svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}

Dies erzeugt einen Konturstrich, der den Timerring wie erwartet abdeckt, aber er animiert sich noch nicht, um den Timerring freizulegen, während die Zeit vergeht.

Um die Länge der Linie der verbleibenden Zeit zu animieren, werden wir die Eigenschaft stroke-dasharray verwenden. Chris erklärt, wie sie verwendet wird, um die Illusion zu erwecken, dass sich ein Element selbst "zeichnet". Und es gibt weitere Details zur Eigenschaft und Beispiele dazu im CSS-Tricks-Almanach.

Schritt 5: Animieren des Fortschrittrings

Sehen wir uns an, wie unser Ring mit verschiedenen stroke-dasharray-Werten aussehen wird

Was wir sehen, ist, dass der Wert von stroke-dasharray unseren Ring für die verbleibende Zeit tatsächlich in gleich lange Abschnitte unterteilt, wobei die Länge dem Wert der verbleibenden Zeit entspricht. Das passiert, wenn wir den Wert von stroke-dasharray auf eine einstellige Zahl (d. h. 1-9) setzen.

Der Name dasharray deutet darauf hin, dass wir mehrere Werte als Array angeben können. Sehen wir uns an, wie es sich verhält, wenn wir stattdessen zwei Zahlen statt einer angeben; in diesem Fall sind diese Werte 10 und 30.

stroke-dasharray: 10 30

Das setzt die Länge des ersten Abschnitts (verbleibende Zeit) auf 10 und die Länge des zweiten Abschnitts (verstrichene Zeit) auf 30. Das können wir mit einem kleinen Trick in unserem Timer verwenden. Was wir anfangs brauchen, ist, dass der Ring die volle Länge des Kreises abdeckt, d. h. die verbleibende Zeit entspricht der Länge unseres Rings.

Was ist das für eine Länge? Holen Sie Ihr altes Geometriebuch hervor, denn wir können die Länge eines Bogens mit etwas Mathematik berechnen

Length = 2πr = 2 * π * 45 = 282,6

Das ist der Wert, den wir verwenden wollen, wenn der Ring anfänglich geladen wird. Sehen wir uns an, wie es aussieht.

stroke-dasharray: 283 283

Das funktioniert!

OK, der erste Wert im Array ist unsere verbleibende Zeit, und der zweite markiert, wie viel Zeit vergangen ist. Was wir jetzt tun müssen, ist, den ersten Wert zu manipulieren. Sehen wir uns unten an, was wir erwarten können, wenn wir den ersten Wert ändern.

Wir werden zwei Methoden erstellen, eine, die dafür verantwortlich ist, zu berechnen, welcher Bruchteil der Anfangszeit noch übrig ist, und eine, die dafür verantwortlich ist, den stroke-dasharray-Wert zu berechnen und das <path>-Element zu aktualisieren, das unsere verbleibende Zeit darstellt.

// Divides time left by the defined time limit.
function calculateTimeFraction() {
  return timeLeft / TIME_LIMIT;
}
    
// Update the dasharray value as time passes, starting with 283
function setCircleDasharray() {
  const circleDasharray = `${(
    calculateTimeFraction() * FULL_DASH_ARRAY
  ).toFixed(0)} 283`;
  document
    .getElementById("base-timer-path-remaining")
    .setAttribute("stroke-dasharray", circleDasharray);
}

Wir müssen auch unseren Pfad jede Sekunde aktualisieren. Das bedeutet, dass wir die neu erstellte Methode setCircleDasharray innerhalb unseres timerInterval aufrufen müssen.

function startTimer() {
  timerInterval = setInterval(() => {
    timePassed = timePassed += 1;
    timeLeft = TIME_LIMIT - timePassed;
    document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
    
    setCircleDasharray();
  }, 1000);
}

Jetzt können wir sehen, wie sich die Dinge bewegen!

Juhu, es funktioniert... aber... schauen Sie genau hin, besonders am Ende. Es sieht so aus, als ob unsere Animation eine Sekunde hinterherhinkt. Wenn wir 0 erreichen, ist noch ein kleines Stück des Rings sichtbar.

Das liegt an der Animationsdauer von einer Sekunde. Wenn der Wert der verbleibenden Zeit auf Null gesetzt wird, dauert es immer noch eine Sekunde, bis der Ring tatsächlich auf Null animiert ist. Wir können das loswerden, indem wir die Länge des Rings während des Countdowns schrittweise reduzieren. Das machen wir in unserer Methode calculateTimeFraction.

function calculateTimeFraction() {
  const rawTimeFraction = timeLeft / TIME_LIMIT;
  return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
}

Da haben wir es!

Hoppla... da ist noch eine Sache. Wir haben gesagt, dass wir die Farbe der Fortschrittsanzeige ändern wollen, wenn die verbleibende Zeit bestimmte Punkte erreicht – so ähnlich wie ein Hinweis an den Benutzer, dass die Zeit fast abgelaufen ist.

Schritt 6: Ändern Sie die Fortschrittsfarbe an bestimmten Zeitpunkten

Zuerst müssen wir zwei Schwellenwerte hinzufügen, die anzeigen, wann wir in den Warn- und Alarmzustand wechseln und Farben für jeden dieser Zustände hinzufügen. Wir beginnen mit Grün, wechseln dann zu Orange als Warnung und dann zu Rot, wenn die Zeit fast abgelaufen ist.

// Warning occurs at 10s
const WARNING_THRESHOLD = 10;
// Alert occurs at 5s
const ALERT_THRESHOLD = 5;

const COLOR_CODES = {
  info: {
    color: "green"
  },
  warning: {
    color: "orange",
    threshold: WARNING_THRESHOLD
  },
  alert: {
    color: "red",
    threshold: ALERT_THRESHOLD
  }
};

Erstellen wir nun eine Methode, die dafür verantwortlich ist zu prüfen, ob der Schwellenwert überschritten wurde und die Fortschrittsfarbe zu ändern, wenn dies geschieht.

function setRemainingPathColor(timeLeft) {
  const { alert, warning, info } = COLOR_CODES;

  // If the remaining time is less than or equal to 5, remove the "warning" class and apply the "alert" class.
  if (timeLeft <= alert.threshold) {
    document
      .getElementById("base-timer-path-remaining")
      .classList.remove(warning.color);
    document
      .getElementById("base-timer-path-remaining")
      .classList.add(alert.color);

  // If the remaining time is less than or equal to 10, remove the base color and apply the "warning" class.
  } else if (timeLeft <= warning.threshold) {
    document
      .getElementById("base-timer-path-remaining")
      .classList.remove(info.color);
    document
      .getElementById("base-timer-path-remaining")
      .classList.add(warning.color);
  }
}

Wir entfernen also im Grunde eine CSS-Klasse, wenn der Timer einen Punkt erreicht, und fügen stattdessen eine andere hinzu. Wir müssen diese Klassen definieren.

.base-timer__path-remaining.green {
  color: rgb(65, 184, 131);
}

.base-timer__path-remaining.orange {
  color: orange;
}

.base-timer__path-remaining.red {
  color: red;
}

Voilà, da haben wir es. Hier ist die Demo wieder mit allem zusammen.