How to Build an SPFx Countdown Timer Web Part (Step-by-Step)

In many of my SharePoint projects, clients often ask for a simple way to display important upcoming dates directly on their intranet home page. Sometimes it’s a product launch, a company-wide event, a compliance deadline, an employee engagement program, or even a major organizational announcement. A countdown timer is one of those small features that can significantly improve visibility and user engagement.

Recently, while working on an SPFx-based intranet portal, I needed a customizable countdown timer that could be managed directly from SharePoint. Although SharePoint Online provides a built-in Countdown Timer web part, it doesn’t always meet every business requirement. In several projects, I found myself building custom solutions using SharePoint Framework (SPFx) and React to provide additional flexibility, branding options, and dynamic functionality.

In this tutorial, I’ll share both approaches that I commonly use in real-world SharePoint implementations:

  • Using the built-in Countdown Timer web part in SharePoint Online (no coding required)
  • Building a custom Countdown Timer web part using SPFx and React for advanced customization

I’ll walk you through each method step by step, including the complete SPFx code, deployment process, and practical implementation guidance. By the end of this article, you’ll have a fully functional countdown timer running on your SharePoint site and a solid understanding of when to use the out-of-the-box solution versus a custom SPFx web part.

SPFx stands for SharePoint Framework. It’s Microsoft’s recommended way to build custom web parts and extensions for SharePoint Online and Microsoft Teams. You write code in TypeScript and React, and it runs client-side directly in the browser.

The great thing about SPFx is that your custom web parts look and behave just like native SharePoint web parts — they show up in the web part toolbox, support the property pane, and they’re fully responsive.

Method 1: The Built-in Countdown Timer Web Part

Before you write a single line of code, it’s worth knowing that SharePoint Online already ships with a Countdown Timer web part out of the box. If this covers your needs, you don’t need to build anything from scratch.

Here’s how to use it:

  1. Open the SharePoint page you want to edit and click Edit in the top-right corner.
  2. Click the + icon where you want to place the timer.
  3. Search for Countdown Timer and select it.
  4. Click the pencil icon (edit) on the web part to open the property pane.
  5. Set your:
    • Title — what the countdown is for (e.g., “Product Launch in”)
    • End date and time — the target datetime
    • Display format — days only, or days + hours + minutes + seconds
    • Count direction — countdown or count up
    • Call-to-action button — optional link you can attach
    • Background image — to make it look more polished
  6. Click Republish to save.
sharepoint deafault count timer web part

That’s it. For most use cases — company events, deadline reminders, open enrollment windows — this works perfectly.

When should you stop here?

The built-in web part is great if:

  • You need a simple countdown with basic branding
  • You don’t want to maintain custom code
  • Non-technical users will be managing the timer

When should you build a custom one?

Move to Method 2 if you need:

  • Auto-repeating timers
  • Custom styling that matches your exact brand guidelines
  • Multiple timers with different behaviors on one page
  • Logic like “show a message when the timer hits zero”

Method 2: Build a Custom SPFx Countdown Timer

Now let’s get into the fun part — building your own countdown timer web part from scratch using SPFx and React.

Prerequisites

Before you start, make sure you have the following installed:

  • Node.js v18 LTS — this is the currently recommended version for SPFx v1.20+
  • Visual Studio Code (or any editor you prefer)
  • Microsoft 365 developer tenant or a SharePoint Online environment you can deploy to

To check if Node is installed:

node --version

Step 1: Set Up Your Development Environment

Open your terminal and install the global tools you need:

npm install @rushstack/heft yo @microsoft/generator-sharepoint --global

This installs:

  • @rushstack/heft — the task runner SPFx uses to build and serve your project
  • yo — Yeoman, the project scaffolding tool
  • @microsoft/generator-sharepoint — the SPFx project generator

Trust the self-signed developer certificate (you’ll need this to run the local workbench):

heft trust-dev-cert

Step 2: Scaffold Your SPFx Project

Create a new folder and scaffold the project:

mkdir spfx-countdown-timer
cd spfx-countdown-timer
yo @microsoft/sharepoint

The Yeoman generator will ask you a few questions. Answer them like this:

PromptAnswer
Solution namespfx-countdown-timer
Type of client-side componentWebPart
Web part nameCountdownTimer
TemplateReact
spfx counter web part creation

This installs all project dependencies. It takes a minute or two.

Step 3: Understand the SPFx Project Structure

After scaffolding, your project will look roughly like this:

spfx-countdown-timer/
├── config/
├── src/
│ └── webparts/
│ └── countdownTimer/
│ ├── CountdownTimerWebPart.ts ← main web part class
│ ├── components/
│ │ ├── CountdownTimer.tsx ← React component
│ │ ├── CountdownTimer.module.scss
│ │ └── ICountdownTimerProps.ts
├── package.json

The key files are:

  • CountdownTimerWebPart.ts — registers the web part with SharePoint, sets up the property pane
  • CountdownTimer.tsx — the React component that actually renders the timer

Step 4: Define the SPFx Web Part Properties

Open CountdownTimerWebPart.ts. We’ll add a property so editors can set the target date from the property pane.

First, add an interface for the properties in your ICountdownTimerWebPartProps.ts file (or inline in the web part file):

export interface ICountdownTimerWebPartProps {
targetDate: string;
title: string;
}

Now update CountdownTimerWebPart.ts to include these properties in the property pane:

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';

import CountdownTimer from './components/CountdownTimer';
import { ICountdownTimerProps } from './components/ICountdownTimerProps';

export interface ICountdownTimerWebPartProps {
targetDate: string;
title: string;
}

export default class CountdownTimerWebPart extends BaseClientSideWebPart<ICountdownTimerWebPartProps> {

public render(): void {
const element: React.ReactElement<ICountdownTimerProps> = React.createElement(
CountdownTimer,
{
targetDate: this.properties.targetDate,
title: this.properties.title || 'Countdown Timer'
}
);

ReactDom.render(element, this.domElement);
}

protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}

protected get dataVersion(): Version {
return Version.parse('1.0');
}

protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: { description: 'Countdown Timer Settings' },
groups: [
{
groupName: 'Configuration',
groupFields: [
PropertyPaneTextField('title', {
label: 'Timer Title',
placeholder: 'e.g. Days Until Product Launch'
}),
PropertyPaneTextField('targetDate', {
label: 'Target Date & Time',
placeholder: 'YYYY-MM-DDTHH:MM:SS e.g. 2025-12-31T09:00:00',
description: 'Enter date and time in ISO format'
})
]
}
]
}
]
};
}
}

Step 5: Build the React Countdown Component

Now let’s build the actual React component. This is where the timer logic lives.

First, update ICountdownTimerProps.ts:

export interface ICountdownTimerProps {
targetDate: string;
title: string;
}

Now open CountdownTimer.tsx and replace everything with this:

import * as React from 'react';
import { useState, useEffect } from 'react';
import { ICountdownTimerProps } from './ICountdownTimerProps';
import styles from './CountdownTimer.module.scss';

interface ITimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
}

const CountdownTimer: React.FC<ICountdownTimerProps> = ({ targetDate, title }) => {

const calculateTimeLeft = (): ITimeLeft | null => {
if (!targetDate) return null;

const target = new Date(targetDate).getTime();
const now = new Date().getTime();
const difference = target - now;

if (difference <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };

return {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((difference % (1000 * 60)) / 1000)
};
};

const [timeLeft, setTimeLeft] = useState<ITimeLeft | null>(calculateTimeLeft());
const [isExpired, setIsExpired] = useState<boolean>(false);

useEffect(() => {
if (!targetDate) return;

const timer = setInterval(() => {
const remaining = calculateTimeLeft();
setTimeLeft(remaining);

if (remaining && remaining.days === 0 && remaining.hours === 0 &&
remaining.minutes === 0 && remaining.seconds === 0) {
setIsExpired(true);
clearInterval(timer);
}
}, 1000);

// Cleanup on unmount — very important in SPFx to avoid memory leaks
return () => clearInterval(timer);
}, [targetDate]);

if (!targetDate) {
return (
<div className={styles.container}>
<p className={styles.placeholder}>Please configure a target date in the web part settings.</p>
</div>
);
}

if (isExpired || !timeLeft) {
return (
<div className={styles.container}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.expired}>🎉 This event has already happened!</p>
</div>
);
}

return (
<div className={styles.container}>
<h2 className={styles.title}>{title}</h2>
<div className={styles.timerGrid}>
<div className={styles.timerBlock}>
<span className={styles.number}>{String(timeLeft.days).padStart(2, '0')}</span>
<span className={styles.label}>Days</span>
</div>
<div className={styles.separator}>:</div>
<div className={styles.timerBlock}>
<span className={styles.number}>{String(timeLeft.hours).padStart(2, '0')}</span>
<span className={styles.label}>Hours</span>
</div>
<div className={styles.separator}>:</div>
<div className={styles.timerBlock}>
<span className={styles.number}>{String(timeLeft.minutes).padStart(2, '0')}</span>
<span className={styles.label}>Minutes</span>
</div>
<div className={styles.separator}>:</div>
<div className={styles.timerBlock}>
<span className={styles.number}>{String(timeLeft.seconds).padStart(2, '0')}</span>
<span className={styles.label}>Seconds</span>
</div>
</div>
</div>
);
};

export default CountdownTimer;

A few things worth noting about this component:

  • I’m using useState to hold the time remaining and a flag for when the timer expires.
  • The useEffect hook starts a setInterval that fires every 1000ms (1 second) to update the time.
  • The cleanup function return () => clearInterval(timer) It is critical; without it, the interval continues to run even after the web part is removed from the page, causing memory leaks in SPFx.
  • padStart(2, '0') makes sure single-digit numbers show as 05 instead of 5, looks much cleaner.

Step 6: Style the Timer

Open CountdownTimer.module.scss and add these styles:

.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
font-family: 'Segoe UI', sans-serif;
background: #0078d4;
border-radius: 8px;
color: #ffffff;
}

.title {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}

.timerGrid {
display: flex;
align-items: center;
gap: 8px;
}

.timerBlock {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(255, 255, 255, 0.15);
border-radius: 6px;
padding: 12px 16px;
min-width: 64px;
}

.number {
font-size: 36px;
font-weight: 700;
line-height: 1;
}

.label {
font-size: 12px;
text-transform: uppercase;
margin-top: 4px;
opacity: 0.8;
letter-spacing: 0.5px;
}

.separator {
font-size: 32px;
font-weight: 700;
align-self: flex-start;
margin-top: 8px;
}

.expired {
font-size: 18px;
font-weight: 500;
}

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

I went with SharePoint’s default blue (#0078d4) so it feels native, but you can swap this to any color your brand uses.

Step 7: Test SPFx Web Part Locally

Run the local dev server:

heft start

This opens a browser with the SharePoint Workbench. Add your CountdownTimer web part from the toolbox. In the property pane on the right, enter a title and a target date in ISO format like 2026-12-31T09:00:00. You should see the timer ticking down in real time.

spfx countdown timer web part

If you see an error about the workbench URL, make sure you’re using https://localhost:4321/temp/workbench.html it and that you’ve already trusted the dev certificate in Step 1.

Step 8: Bundle and Package for Deployment

Once you’re happy with how the timer looks locally, it’s time to package it for SharePoint.

First, build the production bundle:

heft test --clean --production

Then package it into a .sppkg file:

heft package-solution --production

The above command builds an optimized, minified bundle instead of a debug version. After this runs, you’ll find your package at:

sharepoint/solution/spfx-countdown-timer.sppkg

Step 9 — Deploy to SharePoint

  1. Go to your SharePoint Admin Center → More features → Apps → App Catalog.
  2. If you don’t have an App Catalog yet, you’ll be prompted to create one — just follow the wizard.
  3. Click Upload and select your .sppkg file.
  4. When prompted, check “Make this solution available to all sites in the organization” if you want it globally available, or leave it unchecked for manual site-by-site installs.
  5. Click Deploy.

Now go to the SharePoint site where you want to use the web part:

  1. Click Settings (gear icon) → Add an app.
  2. Find your spfx-countdown-timer app and click Add.
  3. Edit any page, click +, search for CountdownTimer, and add it.
  4. Open the property pane, set your title and target date, and you’re done.

Method 3: Add Multiple Countdown Timers with a SharePoint List

Here’s a more advanced use case — what if you want to manage several countdowns from a SharePoint list instead of hardcoding dates in the property pane? This is a pattern I use a lot when a business team needs to update timer dates without bugging a developer.

The Idea

You create a SharePoint list called CountdownEvents with two columns:

Column NameType
TitleSingle line of text
EventDateDate and Time

Your SPFx web part reads this list using the SharePoint REST API and renders a card for each active event.

spfx multiple countdown timer web part

Reading the List with PnPjs

The cleanest way to query SharePoint lists from SPFx is using PnPjs — it’s a wrapper around the SharePoint REST API that saves you from writing verbose fetch calls.

Install it:

npm install @pnp/sp

Initialize PnPjs in your CountdownTimerWebPart.ts:

import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

// Inside your web part class:
private _sp: ReturnType<typeof spfi>;

protected async onInit(): Promise<void> {
this._sp = spfi().using(SPFx(this.context));
return super.onInit();
}

Then fetch the list items and pass them to your component:

public async render(): Promise<void> {
const events = await this._sp.web.lists
.getByTitle("CountdownEvents")
.items
.select("Title", "EventDate")
.filter(`EventDate ge '${new Date().toISOString()}'`)
.orderBy("EventDate", true)();

const element = React.createElement(CountdownTimer, {
events: events,
title: this.properties.title || 'Upcoming Events'
});

ReactDom.render(element, this.domElement);
}

Update the React Component for Multiple Timers

Update ICountdownTimerProps.ts to accept an array:

export interface ICountdownEvent {
Title: string;
EventDate: string;
}

export interface ICountdownTimerProps {
events: ICountdownEvent[];
title: string;
}

Then update CountdownTimer.tsx to render a card per event:

import * as React from 'react';
import { useState, useEffect } from 'react';
import { ICountdownTimerProps, ICountdownEvent } from './ICountdownTimerProps';
import styles from './CountdownTimer.module.scss';

interface ITimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
}

// Reusable single timer card
const TimerCard: React.FC<{ event: ICountdownEvent }> = ({ event }) => {

const calc = (): ITimeLeft => {
const diff = new Date(event.EventDate).getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
return {
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000)
};
};

const [timeLeft, setTimeLeft] = useState<ITimeLeft>(calc());

useEffect(() => {
const interval = setInterval(() => setTimeLeft(calc()), 1000);
return () => clearInterval(interval);
}, [event.EventDate]);

const pad = (n: number): string => String(n).padStart(2, '0');

return (
<div className={styles.card}>
<h3 className={styles.cardTitle}>{event.Title}</h3>
<div className={styles.timerGrid}>
{[
{ value: timeLeft.days, label: 'Days' },
{ value: timeLeft.hours, label: 'Hours' },
{ value: timeLeft.minutes, label: 'Mins' },
{ value: timeLeft.seconds, label: 'Secs' }
].map((item, i, arr) => (
<React.Fragment key={item.label}>
<div className={styles.timerBlock}>
<span className={styles.number}>{pad(item.value)}</span>
<span className={styles.label}>{item.label}</span>
</div>
{i < arr.length - 1 && <span className={styles.separator}>:</span>}
</React.Fragment>
))}
</div>
</div>
);
};

const CountdownTimer: React.FC<ICountdownTimerProps> = ({ events, title }) => {
if (!events || events.length === 0) {
return (
<div className={styles.container}>
<p className={styles.placeholder}>No upcoming events found in the CountdownEvents list.</p>
</div>
);
}

return (
<div className={styles.container}>
<h2 className={styles.title}>{title}</h2>
<div className={styles.cardGrid}>
{events.map((event, index) => (
<TimerCard key={index} event={event} />
))}
</div>
</div>
);
};

export default CountdownTimer;

Add these additional styles to your SCSS file for the multi-card layout:

.cardGrid {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}

.card {
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 16px;
min-width: 280px;
text-align: center;
}

.cardTitle {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}

Now your business team can add, edit, or remove events straight from the SharePoint list — no deployments needed.

Common Errors and Fixes in SPFx

Here are a few issues I’ve run into when building SPFx timers, and how to fix them:

  • Timer keeps running after the web part is removed: This means you forgot the return () => clearInterval(timer) cleanup in your useEffect. Always clean up intervals.
  • “Cannot read property of undefined” on targetDate: This happens when the web part renders before the user sets a date in the property pane. Add a guard check like if (!targetDate) return <p>Configure a date</p>.
  • Date shows wrong time zone: JavaScript new Date() uses the browser’s local timezone. If your users are spread across time zones, consider using UTC explicitly or showing the target timezone label next to the timer.
  • gulp serve throws a certificate error: Run gulp trust-dev-cert again and restart the browser. Sometimes you also need to manually trust the cert in your OS keychain.
  • .sppkg uploads but web part doesn’t appear: Make sure you added the app to the specific site (Settings → Add an app) after deploying to the App Catalog.

Quick Comparison: Which Method Should You Use in SPFx?

SituationBest Method
Simple event countdown, no codingBuilt-in Countdown Timer web part
Full design control, single timerCustom SPFx web part (Method 2)
Multiple timers managed by business usersSPFx + SharePoint List (Method 3)
Timer with custom logic on expiryCustom SPFx web part with callback

Final Thoughts

Building a countdown timer in SPFx is a great first project if you’re just getting into the framework. It touches all the fundamentals, scaffolding a project, building a React component, using hooks, reading from SharePoint, and deploying through the App Catalog.

Start with Method 1 if you just need something to live quickly. Move to Method 2 when you want full control over the look and feel. And once business users start asking to manage their own events, Method 3 is the natural next step.

The cleanup pattern in useEffect is probably the most important thing to get right — it’s easy to skip, but skipping it causes subtle bugs that are hard to track down later.

Also, you may like:

Power Apps functions free pdf

30 Power Apps Functions

This free guide walks you through the 30 most-used Power Apps functions with real business examples, exact syntax, and results you can see.

Download User registration canvas app

DOWNLOAD USER REGISTRATION POWER APPS CANVAS APP

Download a fully functional Power Apps Canvas App (with Power Automate): User Registration App