# React Countdown Timer: Blöcke, Inline, schlafende Tabs

> Bau einen React Countdown Timer als rollende Ziffernblöcke, im Fließtext oder in Worten, und behandle Zeitzonen, schlafende Tabs, Hydration und das Ende sauber.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 19. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-countdown-timer
- Englische Fassung: https://wingo-ui.com/blog/react-countdown-timer

## Kurz gesagt

Ein React Countdown Timer sollte die Restzeit bei jedem Tick aus einem festen Zielzeitpunkt berechnen, statt pro Tick eine Sekunde abzuziehen, denn Browser drosseln Timer in Hintergrund-Tabs. Speicher das Ziel als UTC-ISO-String, render bis zur Hydration einen Platzhalter und lass den Server entscheiden, wann etwas wirklich freigeschaltet wird, nicht den Countdown. Der Countdown von Wingo UI erledigt das und erscheint als Ziffernblöcke, inline im Satz oder in Worten.

Du brauchst einen React Countdown Timer für einen Launch, einen Sale, der um Mitternacht endet, eine Auktion oder eine Testphase. Die erste Version steht in zehn Minuten: eine Zahl im State, `setInterval`, jede Sekunde minus eins. Dann geht sie live. Ein Tab, der im Hintergrund lag, zeigt die falsche Zeit, Berlin und New York sehen den Launch zu verschiedenen Zeitpunkten, und die serverseitig gerenderte Seite meldet eine Mismatch-Warnung. Dieser Guide zeigt die drei Formen, in denen ein Countdown meist auftaucht, die Fallstricke hinter diesen Bugs und die Props, auf die es beim [Countdown](https://wingo-ui.com/components/countdown) von Wingo UI ankommt.

## Wie sollte ein React Countdown Timer die Restzeit berechnen?

Immer aus dem Ziel. Halte einen festen Zeitpunkt (`to`) fest und berechne bei jedem Tick `to - Date.now()`. Der Tick entscheidet nur, wann neu gezeichnet wird; die Zeit selbst trägt er nie. Die Version, die eine Restzahl speichert und herunterzählt, ist die, die driftet.

Ein minimaler Hook, der das richtig macht, sieht so aus:

```tsx
"use client"

import { useEffect, useState } from "react"

// ms bis `to`, oder null, solange die Uhr des Browsers unbekannt ist
export function useTimeLeft(to: number) {
  const [now, setNow] = useState<number | null>(null)
  useEffect(() => {
    const tick = () => setNow(Date.now())
    tick()
    const id = window.setInterval(tick, 1000)
    return () => window.clearInterval(id)
  }, [])
  return now === null ? null : Math.max(0, to - now)
}
```

Zwei Haken hat er noch. Das Intervall läuft in seinem eigenen Sekundenraster, also kann die Anzeige bis zu einer Sekunde vom echten Wert abweichen und die Null zu spät erreichen. Und er startet mit `null`, also musst du entscheiden, was auf dem Server gerendert wird. Eine vollständige React Countdown Komponente kümmert sich um beides und um die Punkte weiter unten.

## Welcher Countdown-Stil passt: Blöcke, inline oder in Worten?

Entscheide danach, wo der Countdown steht. Der Countdown hat drei Varianten:

| Variante | Rendert | Gut für |
|---|---|---|
| `blocks` (Standard) | Eine Kachel pro Einheit mit Beschriftung: `2` DAYS `04` HOURS `12` MINUTES `09` SECONDS | Launch-Hero, Sale-Banner, Wartungsseite |
| `inline` | `2d 04:12:09` im Fluss eines Satzes | „Auktion endet in 3:12:45“ auf einer Angebotskarte |
| `text` | Worte: „in 2 days and 4 hours“ | Hinweise zur Testphase, E-Mails, alles, was ruhig wirken soll |

Blöcke rollen jede geänderte Ziffer wie ein Kilometerzähler an ihren Platz und nutzen dafür den kostenlosen [Animated Counter](https://wingo-ui.com/components/animated-counter) (`animation="fade"` blendet stattdessen über, `"none"` wechselt einfach den Text). Führende Einheiten, die auf null stehen, fallen mit der Zeit weg: Ein Launch in zwei Tagen zeigt Tage, die letzte Stunde nur noch Minuten und Sekunden. „0 days“ erscheint nie.

> Live-Demo (Countdown): Wähl unter der Karte in 10 s, in 2 days, in 45 days oder past: Der Countdown tickt, endet oder zeigt gleich den fertigen Zustand; im Playground wechselst du Variante und Kachelstil. Probier sie unter [Countdown](https://wingo-ui.com/components/countdown) aus und installier sie mit `npx wingo-ui@latest add countdown`.

In einem Launch-Hero ist der Countdown selbst ein einziges Element:

```tsx
import { Countdown } from "@/components/ui/countdown"

export function LaunchHero() {
  return (
    <section className="flex flex-col items-center gap-6 py-16 text-center">
      <h1 className="text-4xl font-semibold tracking-tight">Acme Mobile kommt bald</h1>
      <Countdown to="2026-11-17T17:00:00Z" size="lg" tone="brand" completedContent="Acme Mobile ist live" />
    </section>
  )
}
```

Die Varianten inline und text rendern ein `<span>`, also passen sie in einen Absatz. Die Variante blocks rendert ein `<div>`, und ein `<div>` in einem `<p>` ist ungültiges HTML, das Next.js als Hydration-Fehler meldet. Setz die Blöcke deshalb nie in einen Absatz.

```tsx
<p className="text-sm text-muted-foreground">
  Auktion endet in <Countdown to={auction.endsAt} variant="inline" size="sm" />
</p>

<p className="text-sm">
  Deine Testphase endet <Countdown to={trial.endsAt} variant="text" showSeconds={false} />.
</p>
```

`precision` legt fest, wie viele Einheiten die Variante text nennt (standardmäßig 2, „in 3 days and 4 hours“). `showSeconds={false}` blendet die Sekunden aus und tickt einmal pro Minute. Das ist ruhiger und schont die Performance in einer langen Kartenliste.

> Live-Demo (Countdown): Die Variante text in einem Satz: Launch starts in 2 days and 4 hours. Wähl in 10 s, damit die Worte zu Time's up wechseln. Probier sie unter [Countdown](https://wingo-ui.com/components/countdown) aus und installier sie mit `npx wingo-ui@latest add countdown`.

## Warum driftet ein Countdown in einem Hintergrund-Tab?

Weil Browser Timer in Tabs drosseln, die niemand ansieht. Laut [Chromes Hinweisen zur Timer-Drosselung](https://developer.chrome.com/blog/timer-throttling-in-chrome-88) werden Timer auf einer verborgenen Seite einmal pro Sekunde geprüft. Ab Chrome 88 nur noch einmal pro Minute, wenn die Seite länger als 5 Minuten verborgen ist, seit 30 Sekunden keinen Ton abspielt, ihre Timer 5 oder mehr Ebenen tief verkettet sind und kein WebRTC läuft. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) nennt ein Minimum von 1 Sekunde in inaktiven Tabs von Firefox am Desktop und 15 Minuten in Firefox für Android. Ein Laptop im Ruhezustand führt gar keine Timer aus.

Ein Countdown, der pro Tick eins abzieht, verliert jeden übersprungenen Tick. Einer, der aus dem Ziel neu rechnet, zeichnet nur verspätet neu. Der Countdown geht einen Schritt weiter: Er plant jedes Neuzeichnen genau für den Moment, in dem der Wert eine Einheitengrenze des Ziels überschreitet, damit der letzte Tick genau auf null fällt. Und er rechnet sofort neu, sobald der Tab wieder sichtbar wird, also springt ein Tab, der geschlafen hat, direkt auf den richtigen Wert.

Für lange Countdowns gibt es eine zweite Timer-Falle. `setTimeout` speichert seine Verzögerung als vorzeichenbehaftete 32-Bit-Ganzzahl, das Maximum liegt also bei 2.147.483.647 ms, etwa 24,8 Tagen. Laut MDN laufen größere Werte über, und eine übergelaufene Verzögerung kann sofort feuern. Ein `setTimeout(openSale, saleAt - Date.now())` für einen Sale in 45 Tagen kann also sofort auslösen. Der Countdown wartet nie länger als einen Tick und begrenzt jede einzelne Wartezeit auf dieses Limit. Deshalb zählt ein Launch in 45 Tagen ganz normal herunter.

## Wie gehe ich bei einem Launch Countdown mit Zeitzonen um?

Speicher einen absoluten Zeitpunkt und formatier ihn pro Besucher. Ein Launch am 17. November 2026 um 12 Uhr mittags in New York ist `2026-11-17T17:00:00Z` (Eastern Time ist dann UTC-5). Jeder Browser zählt auf denselben Moment herunter, also endet der Countdown überall gleichzeitig.

Zwei Formate solltest du vermeiden (siehe [MDNs Seite zu Date.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse)):

- **Ein Datum mit Uhrzeit ohne Offset** (`"2026-11-17T12:00:00"`) wird auf jedem Gerät als Ortszeit gelesen. In Berlin steht der Countdown sechs Stunden früher auf null als in New York.
- **Ein reines Datum** (`"2026-11-17"`) wird als Mitternacht UTC gelesen, und das ist in New York der 16. November um 19 Uhr.

Wenn du die Launch-Zeit neben dem Countdown ausgibst, formatier sie in der Zeitzone der jeweiligen Besucher und nenn die Zone, damit „Nov 17, 2026, 12:00 PM EST“ für alle dasselbe bedeutet:

```ts
const launchLabel = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  year: "numeric",
  hour: "numeric",
  minute: "2-digit",
  timeZoneName: "short",
}).format(new Date("2026-11-17T17:00:00Z"))
```

Schreib die Felder hier einzeln aus. `dateStyle` und `timeStyle` lassen sich nicht mit `timeZoneName` kombinieren; versuchst du es trotzdem, gibt es einen `TypeError`.

## Wie rendere ich einen Countdown mit Server-Side Rendering?

Render auf dem Server einen Platzhalter und die Ziffern erst nach der Hydration. Der Server kennt die Uhr der Besucher nicht, und eine statisch vorgerenderte Seite würde die Zeit einfrieren, die sie beim Build berechnet hat. Die [Next.js-Seite zum Hydration-Fehler](https://nextjs.org/docs/messages/react-hydration-error) nennt zeitabhängige APIs wie den `Date()`-Konstruktor in der Render-Logik als häufige Ursache für Mismatches.

Der Countdown liest die Uhr über `useSyncExternalStore` mit einem Server-Snapshot von `null`. Bis die Uhr bekannt ist, rendert jede Einheit `--` über einem unsichtbaren `00` in ihrer endgültigen Breite. So springt nichts, wenn die echten Ziffern kommen, und es wird kein Mismatch gemeldet. Das Attribut `data-state` am Root-Element steht dann auf `idle`, während des Zählens auf `running` und bei null auf `done`.

`now` friert die Uhr für Screenshots und Tests ein; ein so fixierter Countdown tickt nicht.

## Was passiert, wenn der Countdown null erreicht?

`onComplete` feuert einmal beim Übergang auf null, pünktlich auch bei ausgeblendeten Sekunden. `completedContent` ersetzt die Ziffern und federt herein. Liegt `to` beim Mounten schon in der Vergangenheit, rendert die Komponente den fertigen Zustand, ohne `onComplete` aufzurufen. Wer die Seite nach dem Ende des Sales öffnet, bekommt also keinen Toast „Sale gerade beendet“.

```tsx
"use client"

import { useRouter } from "next/navigation"
import { Countdown } from "@/components/ui/countdown"

export function SaleTimer({ endsAt }: { endsAt: string }) {
  const router = useRouter()
  return (
    <Countdown
      to={endsAt}
      size="sm"
      tone="danger"
      completedContent="Das Angebot ist abgelaufen"
      // die Seite nach dem Sale vom Server holen
      onComplete={() => router.refresh()}
    />
  )
}
```

Der Countdown ist eine reine Anzeige. Er vertraut der Uhr des Geräts, und die kann Minuten danebenliegen oder von Hand verstellt sein. Wenn der Moment wichtig ist (ein Preis im Checkout, das Ende einer Auktion, Early Access), prüft der Server die Zeit, wenn die Anfrage ankommt. Der Countdown sagt den Leuten nur, wann sie wiederkommen sollen.

## Gibt es einen React Countdown Hook für eigene Layouts?

Ja. `useCountdown` ist die Uhr hinter der Komponente und wird aus derselben Datei exportiert. Er gibt `{ days, hours, minutes, seconds, totalMs, done }` zurück, bei jedem Tick aus der absoluten Zeit neu berechnet, wobei `totalMs` `null` ist, bis die Uhr bekannt ist. Seine Optionen sind `now`, `interval` (Standard 1000), `onComplete` und `units`. Mit `units: ["seconds"]` tragen die Sekunden den ganzen Rest, und das passt zu einem Timer für das erneute Senden eines Codes:

```tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { useCountdown } from "@/components/ui/countdown"

export function ResendCode({ onResend }: { onResend: () => void }) {
  const [to, setTo] = useState(() => Date.now() + 60_000)
  const { seconds, done, totalMs } = useCountdown(to, { units: ["seconds"] })
  const left = totalMs === null ? 60 : seconds

  return (
    <Button
      variant="soft"
      disabled={!done}
      onClick={() => {
        onResend()
        setTo(Date.now() + 60_000)
      }}
    >
      {done ? "Code erneut senden" : `Erneut senden in ${left} s`}
    </Button>
  )
}
```

Die Doku des Countdowns kombiniert diesen React Countdown Hook mit dem kostenlosen [Progress](https://wingo-ui.com/components/progress)-Ring für einen 60-Sekunden-Timer, nach dem sich der OTP-Code erneut senden lässt, dasselbe Muster wie im [Guide zum React OTP Input](https://wingo-ui.com/de/blog/react-otp-input).

## Wie funktioniert der Countdown mit Screenreadern und auf dem Smartphone?

Das Root-Element hat `role="timer"` mit `aria-live="off"`, denn eine Live-Region, die sich jede Sekunde ändert, würde Screenreader-Nutzer jede Sekunde unterbrechen. Ein versteckter Satz („2 days, 4 hours and 12 minutes left“) aktualisiert sich einmal pro Minute für alle, die dorthin navigieren, und das Ende wird einmal als höfliche Meldung (`polite`) angesagt. Bei reduzierter Bewegung wechseln die Ziffern einfach, statt zu rollen.

Auf dem Smartphone verkleinern die `lg`-Blöcke ihre Ziffern und Kacheln, sobald sie selbst schmaler als 400px sind, damit vier Einheiten weiterhin auf einen Bildschirm mit 360px passen. Das Root-Element trägt in der letzten Minute `data-urgent`, für einen Farbwechsel kurz vor dem Ende. Jeder Text steckt in `labels` (`days`, `hours`, `minutes`, `seconds`, `names`, `short`, `sentence`, `done`, `and`, `in`), mit Pluralfunktionen pro Einheit. Eine Übersetzung ist also ein Prop statt eines Forks.

## Wie schneiden die React Countdown Libraries im Vergleich ab?

react-countdown ist die Headless-Option, react-countdown-circle-timer zeichnet einen Ring, und der Countdown von Wingo UI gibt dir fertige Layouts als Quellcode. Stand Oktober 2026, laut dem öffentlichen README des jeweiligen Projekts:

| Option | Modell | Zählt | Hinweise |
|---|---|---|---|
| [react-countdown](https://github.com/ndresx/react-countdown) | MIT-Paket, Komponente plus `useCountdown`-Hook | Bis zu einem `date` | Prop `renderer` für dein eigenes Markup, `intervalDelay`, `overtime` über null hinaus |
| [react-countdown-circle-timer](https://github.com/vydimitrov/react-countdown-circle-timer) | MIT-Paket, Komponente plus `useCountdown`-Hook | Eine `duration` in Sekunden | Ein SVG-Ring mit Farbstufen; der Hook liefert die Werte für den Pfad |
| Ein eigener Hook | Dein Code | Was du schreibst | Reicht für einen einfachen Timer, solange er aus dem Ziel neu rechnet |
| [Countdown](https://wingo-ui.com/components/countdown) von Wingo UI | Pro-Komponente als Quellcode in deinem Repo | Bis `to` | Varianten blocks, inline und text, Platzhalter für SSR, ausgerichtete Ticks, Labels |

Für einen Ring um einen Quiz-Timer mit 30 Sekunden passt react-countdown-circle-timer besser. Für Headless-Logik mit komplett eigenem Markup ist react-countdown eine solide Wahl.

## Wo passt der Countdown auf eine Launch-Seite?

Ganz oben auf eine Wartelisten-Seite. Die [Vorlage Waitlist Page](https://wingo-ui.com/templates/waitlist-page) setzt einen Countdown unter die Überschrift, der bis `launchAt` zählt, mit einem „live“-Hinweis als Inhalt für den fertigen Zustand, und wird zu einer einfachen Coming-soon-Seite, wenn `launchAt` `null` ist. Das [Tutorial zur Next.js Waitlist Page](https://wingo-ui.com/de/blog/nextjs-waitlist-page) verbindet sie mit einer echten Anmeldetabelle, der Position auf der Warteliste und Empfehlungslinks. So sammelt die Seite mit dem Launch Countdown, die du vor dem ersten Tag live stellst, Anmeldungen, statt nur zu ticken. Für eine einzelne rollende Zahl statt eines ganzen Timers gibt es den [Guide zum animierten Zahlenzähler](https://wingo-ui.com/de/blog/react-animated-number-counter), und für relative Angaben wie „vor 3 Stunden“ gibt es die Komponente [Relative Time](https://wingo-ui.com/components/relative-time).

## Wie baue ich ihn in mein Projekt ein?

Führ `npx wingo-ui@latest add countdown` aus. Die CLI kopiert `components/ui/countdown.tsx` in dein Projekt, dazu den Animated Counter und die Helfer, die er importiert, und installiert `motion`. Der Countdown und die Waitlist Page gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing). Weitere Deep Dives zu einzelnen Komponenten findest du in der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components).

## Komponenten in diesem Artikel

- [Countdown](https://wingo-ui.com/components/countdown) (Pro): A React countdown timer to a launch, sale or deadline, shown as rolling digit blocks, inline in a sentence or in words, with done content. Installation: `npx wingo-ui@latest add countdown`
- [Waitlist Page](https://wingo-ui.com/templates/waitlist-page) (Pro): React waitlist page template: a launch countdown, an email signup with social proof, your place in line with a referral link and FAQ. Installation: `npx wingo-ui@latest add waitlist-page`

## Häufige Fragen

### Wie baue ich einen Countdown Timer in React?

Halte den Zielzeitpunkt fest (einen Timestamp oder einen ISO-String mit Offset) und berechne in einem useEffect bei jedem Tick Ziel minus Date.now(), dann teil das Ergebnis in Tage, Stunden, Minuten und Sekunden auf. Zähl nie einen Zähler herunter, denn ein gedrosselter oder schlafender Tab überspringt Ticks, und der Zähler hinkt hinterher.

### Warum läuft mein Countdown Timer in einem Hintergrund-Tab langsamer?

Browser drosseln Timer in verborgenen Tabs: Chrome prüft sie einmal pro Sekunde und nur noch einmal pro Minute, wenn eine Seite länger als 5 Minuten verborgen ist und ihre Timer verkettet sind. Ein Countdown, der pro Tick eins abzieht, verliert die übersprungenen Ticks; einer, der aus dem Ziel neu rechnet, zeigt beim nächsten Tick den richtigen Wert.

### Wie zeige ich denselben Countdown in jeder Zeitzone?

Speicher den Launch als einen absoluten Zeitpunkt, etwa 2026-11-17T17:00:00Z, und lass jeden Browser darauf herunterzählen. Ein Datum mit Uhrzeit ohne Offset wird als Ortszeit der Besucher gelesen und ein reines Datum als Mitternacht UTC. Mit beiden Formaten endet der Countdown je nach Zeitzone zu einem anderen Zeitpunkt.

### Gibt es einen React Countdown Hook?

Ja. Das Paket react-countdown exportiert useCountdown, und der Countdown von Wingo UI exportiert einen useCountdown-Hook, der days, hours, minutes, seconds, totalMs und done für dein eigenes Layout zurückgibt. Ein kurzer eigener Hook geht auch, solange er aus dem Ziel neu rechnet.

### Ist der Countdown von Wingo UI kostenlos?

Nein. Der Countdown und die Vorlage Waitlist Page sind Pro. Der Animated Counter, der seine Ziffern rollt, und der Progress-Ring aus dem Beispiel zum erneuten Senden sind kostenlos.

---

Quelle: https://wingo-ui.com/de/blog/react-countdown-timer
