image

09 Jan 2026

9K

35K

Creating an Event Countdown Timer Widget in Flutter

Countdown timers are a common and engaging UI element in many applications. Whether it's for an upcoming product launch, a special event, or a limited-time offer, a well-designed countdown timer can effectively build anticipation and inform users. In Flutter, building such a widget is straightforward thanks to its reactive framework and powerful tooling for time management.

This article will guide you through creating a reusable and customizable event countdown timer widget in Flutter, covering the core concepts of state management, time calculation, and UI rendering.

Prerequisites

  • Basic understanding of Flutter and Dart.
  • A Flutter development environment set up.

Core Concepts

To build our countdown timer, we'll primarily rely on the following Dart and Flutter features:

  • StatefulWidget: Essential for managing the timer's state, as the displayed time will change every second.
  • DateTime: Used to represent specific points in time, both for the current moment and the target event time.
  • Duration: Represents the difference between two DateTime objects, providing methods to extract days, hours, minutes, and seconds.
  • Timer from dart:async: Used to trigger periodic updates to the UI, typically every second.

Step-by-Step Implementation

1. Project Setup

If you don't have a Flutter project, create one:


flutter create countdown_app
cd countdown_app

Then, open lib/main.dart. We'll create our custom widget in a separate file, e.g., lib/countdown_timer_widget.dart.

2. Define the Widget Structure

Our countdown timer needs to be a StatefulWidget to update its display dynamically. Create lib/countdown_timer_widget.dart and add the basic structure:


import 'dart:async';
import 'package:flutter/material.dart';

class CountdownTimerWidget extends StatefulWidget {
  final DateTime eventDateTime;
  final TextStyle? textStyle;
  final String? expiredMessage;

  const CountdownTimerWidget({
    Key? key,
    required this.eventDateTime,
    this.textStyle,
    this.expiredMessage,
  }) : super(key: key);

  @override
  _CountdownTimerWidgetState createState() => _CountdownTimerWidgetState();
}

class _CountdownTimerWidgetState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(); // Placeholder for now
  }
}

We've added some properties: eventDateTime (the target date), textStyle for customization, and expiredMessage for when the countdown finishes.

3. Initialize State and Timer

Inside _CountdownTimerWidgetState, we need to manage the remaining duration and the timer itself. We'll calculate the initial duration in initState and set up a periodic timer.


import 'dart:async';
import 'package:flutter/material.dart';

class CountdownTimerWidget extends StatefulWidget {
  final DateTime eventDateTime;
  final TextStyle? textStyle;
  final String? expiredMessage;

  const CountdownTimerWidget({
    Key? key,
    required this.eventDateTime,
    this.textStyle,
    this.expiredMessage,
  }) : super(key: key);

  @override
  _CountdownTimerWidgetState createState() => _CountdownTimerWidgetState();
}

class _CountdownTimerWidgetState extends State {
  late Duration _remainingDuration;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _startTimer();
  }

  @override
  void dispose() {
    _timer?.cancel(); // Cancel the timer to prevent memory leaks
    super.dispose();
  }

  void _startTimer() {
    _updateRemainingTime(); // Initial update
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _updateRemainingTime();
    });
  }

  void _updateRemainingTime() {
    setState(() {
      _remainingDuration = widget.eventDateTime.difference(DateTime.now());
      if (_remainingDuration.isNegative) {
        _remainingDuration = Duration.zero; // Ensure duration doesn't go negative
        _timer?.cancel(); // Stop the timer once the event has passed
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    // We'll implement this in the next step
    return Container();
  }
}

Key points here:

  • _remainingDuration stores the time left.
  • _timer holds our periodic timer.
  • initState calls _startTimer() when the widget is created.
  • dispose cancels the timer when the widget is removed from the tree, preventing resource leaks.
  • _startTimer initializes _remainingDuration and then sets up a Timer.periodic to call _updateRemainingTime every second.
  • _updateRemainingTime calculates the difference between the target event and the current time. If the difference is negative, it means the event has passed, so we set the duration to zero and cancel the timer.

4. Displaying the Countdown

Now, let's implement the build method to display the formatted countdown. We'll extract days, hours, minutes, and seconds from _remainingDuration.


// ... (previous code)

class _CountdownTimerWidgetState extends State {
  late Duration _remainingDuration;
  Timer? _timer;

  // ... (initState, dispose, _startTimer, _updateRemainingTime methods)

  @override
  Widget build(BuildContext context) {
    if (_remainingDuration == Duration.zero && widget.expiredMessage != null) {
      return Text(
        widget.expiredMessage!,
        style: widget.textStyle ?? Theme.of(context).textTheme.headlineSmall,
      );
    }

    final int days = _remainingDuration.inDays;
    final int hours = _remainingDuration.inHours % 24;
    final int minutes = _remainingDuration.inMinutes % 60;
    final int seconds = _remainingDuration.inSeconds % 60;

    final defaultTextStyle = Theme.of(context).textTheme.headlineSmall;

    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        _buildTimeSegment(days, 'Days', widget.textStyle ?? defaultTextStyle),
        _buildSeparator(widget.textStyle ?? defaultTextStyle),
        _buildTimeSegment(hours, 'Hours', widget.textStyle ?? defaultTextStyle),
        _buildSeparator(widget.textStyle ?? defaultTextStyle),
        _buildTimeSegment(minutes, 'Minutes', widget.textStyle ?? defaultTextStyle),
        _buildSeparator(widget.textStyle ?? defaultTextStyle),
        _buildTimeSegment(seconds, 'Seconds', widget.textStyle ?? defaultTextStyle),
      ],
    );
  }

  Widget _buildTimeSegment(int value, String unit, TextStyle style) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(
          value.toString().padLeft(2, '0'), // Pad with '0' for single digits
          style: style.copyWith(fontSize: style.fontSize ?? 24)
        ),
        Text(
          unit,
          style: style.copyWith(fontSize: (style.fontSize ?? 24) * 0.5, fontWeight: FontWeight.normal)
        ),
      ],
    );
  }

  Widget _buildSeparator(TextStyle style) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 4.0),
      child: Text(
        ':',
        style: style.copyWith(fontSize: style.fontSize ?? 24)
      ),
    );
  }
}

In this enhanced build method:

  • We check if the duration is zero and display an expiredMessage if provided.
  • We calculate days, hours, minutes, and seconds using modulo arithmetic for hours, minutes, and seconds to keep them within their respective ranges.
  • Helper methods _buildTimeSegment and _buildSeparator are used to structure the display, providing flexibility for styling.
  • padLeft(2, '0') ensures single-digit numbers are padded with a leading zero (e.g., 5 becomes 05).

5. Integrating into main.dart

Now, let's use our new widget in the main application. Replace the content of your lib/main.dart with the following:


import 'package:flutter/material.dart';
import 'package:countdown_app/countdown_timer_widget.dart'; // Import your widget

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Countdown Timer',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const CountdownHomePage(),
    );
  }
}

class CountdownHomePage extends StatelessWidget {
  const CountdownHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Set your event date and time here.
    // Example: December 25th, 2024, at 09:00 AM
    final DateTime christmas2024 = DateTime(2024, 12, 25, 9, 0, 0);

    // Example: An event that has already passed
    final DateTime pastEvent = DateTime(2023, 1, 1, 0, 0, 0);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Event Countdown'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Countdown to Christmas 2024:',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            CountdownTimerWidget(
              eventDateTime: christmas2024,
              textStyle: const TextStyle(
                fontSize: 32,
                fontWeight: FontWeight.bold,
                color: Colors.deepPurple,
              ),
              expiredMessage: 'Merry Christmas! The event has begun!',
            ),
            const SizedBox(height: 50),
            const Text(
              'Countdown to a past event:',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            CountdownTimerWidget(
              eventDateTime: pastEvent,
              textStyle: const TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.w600,
                color: Colors.grey,
              ),
              expiredMessage: 'This event has already happened!',
            ),
          ],
        ),
      ),
    );
  }
}

Now, run your Flutter application:


flutter run

You should see two countdown timers, one counting down to a future event and another displaying the "expired" message for a past event.

Customization and Enhancements

This basic widget provides a solid foundation. Here are some ideas for further customization and enhancements:

  • Customizable Separators: Allow users to specify a custom widget or string for separators instead of just a colon.
  • Different Layouts: Provide options for horizontal or vertical layouts, or even a single text string format.
  • Completion Callback: Add a Function() onCountdownComplete; property that gets called when _remainingDuration becomes Duration.zero.
  • Styling Individual Units: Pass distinct TextStyle objects for days, hours, minutes, and seconds if more granular control is needed.
  • Internationalization: Adapt the "Days", "Hours" labels based on the user's locale.
  • Animations: Add subtle animations when the numbers change.

Conclusion

You've successfully built a dynamic and reusable event countdown timer widget in Flutter. By combining StatefulWidget for state management, DateTime and Duration for time calculations, and Timer for periodic updates, you can create engaging and informative UI elements. This widget serves as a great starting point, and you can easily expand its features and styling to fit any application's needs.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is