Tutorials 18 min

Building a World Clock Widget: Step-by-Step HTML/CSS/JavaScript Tutorial

๐Ÿ“– In this article: Create a fully functional world clock widget from scratch. Complete tutorial with live code examples, customization options, and deployment guide.

Project Setup and Requirements

In this practical tutorial, we'll build a fully functional world clock widget that displays multiple time zones simultaneously. This project will teach you essential web development skills while creating something genuinely useful.

What We're Building

๐ŸŒ Project Features
  • Multiple time zones: Display 4-6 major cities simultaneously
  • Real-time updates: Clocks update every second
  • Interactive controls: Add/remove cities, toggle 12/24-hour format
  • Responsive design: Works on desktop, tablet, and mobile
  • Theme switching: Light/dark mode toggle
  • No external dependencies: Pure HTML, CSS, and JavaScript

Prerequisites

To follow this tutorial, you should have:

  • Basic HTML knowledge: Understanding of tags and structure
  • CSS fundamentals: Selectors, properties, and layout basics
  • JavaScript basics: Variables, functions, and DOM manipulation
  • Text editor: VS Code, Sublime Text, or any code editor
  • Web browser: Chrome, Firefox, or Safari for testing

Project Structure

Create a new folder and set up these files:

world-clock-widget/
โ”œโ”€โ”€ index.html
โ”œโ”€โ”€ css/
โ”‚   โ””โ”€โ”€ style.css
โ”œโ”€โ”€ js/
โ”‚   โ””โ”€โ”€ app.js
โ””โ”€โ”€ README.md

Building the HTML Structure

Step 1: Create the Base HTML

Create index.html with this structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>World Clock Widget</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <div class="world-clock-container">
        <header class="clock-header">
            <h1>๐ŸŒ World Clock</h1>
            <div class="controls">
                <button id="toggle-format" class="btn">12/24 Hour</button>
                <button id="toggle-theme" class="btn">๐ŸŒ™ Theme</button>
                <button id="add-city" class="btn">+ Add City</button>
            </div>
        </header>
        
        <main class="clocks-grid" id="clocks-container">
            <!-- Clock cards will be generated by JavaScript -->
        </main>
        
        <div class="add-city-modal" id="city-modal">
            <div class="modal-content">
                <h3>Add New City</h3>
                <select id="timezone-select">
                    <option value="">Select a timezone...</option>
                </select>
                <div class="modal-buttons">
                    <button id="add-city-confirm" class="btn btn-primary">Add</button>
                    <button id="cancel-add-city" class="btn btn-secondary">Cancel</button>
                </div>
            </div>
        </div>
    </div>
    
    <script src="js/app.js"></script>
</body>
</html>

Styling with CSS

Step 2: Create Beautiful Styles

Create css/style.css with this comprehensive styling:

/* Reset and variables */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --primary-color: #2563eb;
    --secondary-color: #64748b;
    --background-color: #f8fafc;
    --surface-color: #ffffff;
    --text-primary: #1e293b;
    --text-secondary: #64748b;
    --border-color: #e2e8f0;
    --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
    --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Dark theme */
[data-theme="dark"] {
    --primary-color: #3b82f6;
    --secondary-color: #94a3b8;
    --background-color: #0f172a;
    --surface-color: #1e293b;
    --text-primary: #f1f5f9;
    --text-secondary: #94a3b8;
    --border-color: #334155;
    --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3);
}

/* Base styles */
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: var(--background-color);
    color: var(--text-primary);
    line-height: 1.6;
    transition: var(--transition);
}

.world-clock-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
    min-height: 100vh;
}

/* Header */
.clock-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 2rem;
    padding-bottom: 1rem;
    border-bottom: 2px solid var(--border-color);
}

.clock-header h1 {
    font-size: 2.5rem;
    font-weight: 700;
    background: linear-gradient(135deg, var(--primary-color), #8b5cf6);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

.controls {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
}

/* Buttons */
.btn {
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: 0.5rem;
    font-weight: 500;
    cursor: pointer;
    transition: var(--transition);
    background-color: var(--surface-color);
    color: var(--text-primary);
    border: 2px solid var(--border-color);
    font-size: 0.875rem;
}

.btn:hover {
    background-color: var(--primary-color);
    color: white;
    border-color: var(--primary-color);
    transform: translateY(-1px);
}

.btn-primary {
    background-color: var(--primary-color);
    color: white;
    border-color: var(--primary-color);
}

.btn-secondary {
    background-color: var(--secondary-color);
    color: white;
    border-color: var(--secondary-color);
}

/* Clock grid */
.clocks-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    margin-bottom: 2rem;
}

/* Clock cards */
.clock-card {
    background-color: var(--surface-color);
    border-radius: 1rem;
    padding: 1.5rem;
    box-shadow: var(--shadow);
    border: 1px solid var(--border-color);
    transition: var(--transition);
    position: relative;
    overflow: hidden;
}

.clock-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 4px;
    background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
}

.clock-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
}

.clock-header-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
}

.city-name {
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--text-primary);
}

.remove-city {
    background: none;
    border: none;
    font-size: 1.25rem;
    cursor: pointer;
    color: var(--text-secondary);
    padding: 0.25rem;
    border-radius: 0.25rem;
    transition: var(--transition);
}

.remove-city:hover {
    background-color: #fee2e2;
    color: #dc2626;
}

.time-display {
    text-align: center;
    margin: 1.5rem 0;
}

.current-time {
    font-size: 2.5rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--primary-color);
    margin-bottom: 0.5rem;
    font-family: 'SF Mono', Monaco, monospace;
}

.current-date {
    font-size: 1rem;
    color: var(--text-secondary);
    font-weight: 500;
}

.timezone-info {
    font-size: 0.875rem;
    color: var(--text-secondary);
    text-align: center;
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border-color);
}

/* Modal */
.add-city-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 1000;
    backdrop-filter: blur(4px);
}

.add-city-modal.active {
    display: flex;
    align-items: center;
    justify-content: center;
    animation: fadeIn 0.3s ease;
}

.modal-content {
    background-color: var(--surface-color);
    padding: 2rem;
    border-radius: 1rem;
    box-shadow: var(--shadow-lg);
    max-width: 400px;
    width: 90%;
    animation: slideUp 0.3s ease;
}

.modal-content h3 {
    margin-bottom: 1.5rem;
    color: var(--text-primary);
}

#timezone-select {
    width: 100%;
    padding: 0.75rem;
    border: 2px solid var(--border-color);
    border-radius: 0.5rem;
    background-color: var(--surface-color);
    color: var(--text-primary);
    margin-bottom: 1.5rem;
    font-size: 1rem;
}

.modal-buttons {
    display: flex;
    gap: 1rem;
    justify-content: flex-end;
}

/* Welcome message */
.welcome-message {
    text-align: center;
    padding: 3rem;
    color: var(--text-secondary);
    grid-column: 1 / -1;
}

.welcome-message h3 {
    font-size: 1.5rem;
    margin-bottom: 1rem;
    color: var(--text-primary);
}

/* Notifications */
.notification {
    position: fixed;
    top: 20px;
    right: 20px;
    padding: 1rem 1.5rem;
    border-radius: 0.5rem;
    color: white;
    font-weight: 500;
    z-index: 1001;
    animation: slideInRight 0.3s ease;
}

.notification-success { background-color: #10b981; }
.notification-error { background-color: #ef4444; }
.notification-info { background-color: var(--primary-color); }

/* Responsive design */
@media (max-width: 768px) {
    .world-clock-container { padding: 1rem; }
    .clock-header { 
        flex-direction: column; 
        gap: 1rem; 
        text-align: center; 
    }
    .clock-header h1 { font-size: 2rem; }
    .controls { justify-content: center; }
    .clocks-grid { 
        grid-template-columns: 1fr; 
        gap: 1rem; 
    }
    .current-time { font-size: 2rem; }
}

/* Animations */
@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes slideUp {
    from { 
        opacity: 0;
        transform: translateY(20px);
    }
    to { 
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes slideInRight {
    from {
        opacity: 0;
        transform: translateX(100%);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

Core JavaScript Functionality

Step 3: Adding Interactivity

Create js/app.js with the complete functionality:

class WorldClock {
    constructor() {
        // Default cities to display
        this.cities = [
            { name: 'New York', timezone: 'America/New_York' },
            { name: 'London', timezone: 'Europe/London' },
            { name: 'Tokyo', timezone: 'Asia/Tokyo' },
            { name: 'Sydney', timezone: 'Australia/Sydney' }
        ];
        
        this.is24Hour = false;
        this.theme = 'light';
        this.updateInterval = null;
        
        this.init();
    }
    
    init() {
        this.loadSettings();
        this.setupEventListeners();
        this.populateTimezoneSelect();
        this.renderClocks();
        this.startUpdating();
    }
    
    // Load saved settings from localStorage
    loadSettings() {
        const savedCities = localStorage.getItem('worldClockCities');
        const saved24Hour = localStorage.getItem('worldClock24Hour');
        const savedTheme = localStorage.getItem('worldClockTheme');
        
        if (savedCities) {
            this.cities = JSON.parse(savedCities);
        }
        
        if (saved24Hour !== null) {
            this.is24Hour = JSON.parse(saved24Hour);
        }
        
        if (savedTheme) {
            this.theme = savedTheme;
            document.documentElement.setAttribute('data-theme', this.theme);
        }
    }
    
    // Save settings to localStorage
    saveSettings() {
        localStorage.setItem('worldClockCities', JSON.stringify(this.cities));
        localStorage.setItem('worldClock24Hour', JSON.stringify(this.is24Hour));
        localStorage.setItem('worldClockTheme', this.theme);
    }
    
    // Set up all event listeners
    setupEventListeners() {
        // Toggle 12/24 hour format
        document.getElementById('toggle-format').addEventListener('click', () => {
            this.is24Hour = !this.is24Hour;
            this.saveSettings();
            this.renderClocks();
        });
        
        // Toggle theme
        document.getElementById('toggle-theme').addEventListener('click', () => {
            this.theme = this.theme === 'light' ? 'dark' : 'light';
            document.documentElement.setAttribute('data-theme', this.theme);
            this.saveSettings();
        });
        
        // Show add city modal
        document.getElementById('add-city').addEventListener('click', () => {
            document.getElementById('city-modal').classList.add('active');
        });
        
        // Hide modal
        document.getElementById('cancel-add-city').addEventListener('click', () => {
            this.hideModal();
        });
        
        // Add city
        document.getElementById('add-city-confirm').addEventListener('click', () => {
            this.addCity();
        });
        
        // Hide modal on backdrop click
        document.getElementById('city-modal').addEventListener('click', (e) => {
            if (e.target.id === 'city-modal') {
                this.hideModal();
            }
        });
        
        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.hideModal();
            }
            if (e.key === 't' && e.ctrlKey) {
                e.preventDefault();
                this.theme = this.theme === 'light' ? 'dark' : 'light';
                document.documentElement.setAttribute('data-theme', this.theme);
                this.saveSettings();
            }
        });
    }
    
    // Get list of available timezones
    getTimezoneList() {
        return [
            { name: 'New York', timezone: 'America/New_York' },
            { name: 'Los Angeles', timezone: 'America/Los_Angeles' },
            { name: 'London', timezone: 'Europe/London' },
            { name: 'Paris', timezone: 'Europe/Paris' },
            { name: 'Berlin', timezone: 'Europe/Berlin' },
            { name: 'Moscow', timezone: 'Europe/Moscow' },
            { name: 'Tokyo', timezone: 'Asia/Tokyo' },
            { name: 'Seoul', timezone: 'Asia/Seoul' },
            { name: 'Hong Kong', timezone: 'Asia/Hong_Kong' },
            { name: 'Singapore', timezone: 'Asia/Singapore' },
            { name: 'Dubai', timezone: 'Asia/Dubai' },
            { name: 'Mumbai', timezone: 'Asia/Kolkata' },
            { name: 'Sydney', timezone: 'Australia/Sydney' },
            { name: 'Melbourne', timezone: 'Australia/Melbourne' }
        ];
    }
    
    // Populate timezone select dropdown
    populateTimezoneSelect() {
        const select = document.getElementById('timezone-select');
        const timezones = this.getTimezoneList();
        
        select.innerHTML = '';
        
        timezones.forEach(tz => {
            const option = document.createElement('option');
            option.value = JSON.stringify(tz);
            option.textContent = `${tz.name} (${tz.timezone})`;
            select.appendChild(option);
        });
    }
    
    // Get current time for a specific timezone
    getCurrentTime(timezone) {
        const now = new Date();
        return {
            time: now.toLocaleTimeString('en-US', {
                timeZone: timezone,
                hour12: !this.is24Hour,
                hour: 'numeric',
                minute: '2-digit',
                second: '2-digit'
            }),
            date: now.toLocaleDateString('en-US', {
                timeZone: timezone,
                weekday: 'long',
                year: 'numeric',
                month: 'long',
                day: 'numeric'
            })
        };
    }
    
    // Render all clock cards
    renderClocks() {
        const container = document.getElementById('clocks-container');
        container.innerHTML = '';
        
        this.cities.forEach(city => {
            const clockCard = this.createClockCard(city);
            container.appendChild(clockCard);
        });
        
        if (this.cities.length === 0) {
            container.innerHTML = `
                <div class="welcome-message">
                    <h3>๐ŸŒ Welcome to World Clock!</h3>
                    <p>Click "Add City" to start tracking time zones around the world.</p>
                </div>
            `;
        }
    }
    
    // Create a single clock card
    createClockCard(city) {
        const card = document.createElement('div');
        card.className = 'clock-card';
        
        const timeData = this.getCurrentTime(city.timezone);
        
        card.innerHTML = `
            <div class="clock-header-info">
                <div class="city-name">${city.name}</div>
                <button class="remove-city" onclick="worldClock.removeCity('${city.timezone}')" title="Remove ${city.name}">
                    โœ•
                </button>
            </div>
            
            <div class="time-display">
                <div class="current-time" data-timezone="${city.timezone}">
                    ${timeData.time}
                </div>
                <div class="current-date" data-timezone="${city.timezone}">
                    ${timeData.date}
                </div>
            </div>
            
            <div class="timezone-info">
                ${city.timezone}
            </div>
        `;
        
        return card;
    }
    
    // Update all displayed times
    updateTimes() {
        document.querySelectorAll('.current-time').forEach(timeElement => {
            const timezone = timeElement.dataset.timezone;
            const city = this.cities.find(c => c.timezone === timezone);
            
            if (city) {
                const timeData = this.getCurrentTime(timezone);
                timeElement.textContent = timeData.time;
                
                const dateElement = timeElement.parentNode.querySelector('.current-date');
                if (dateElement) {
                    dateElement.textContent = timeData.date;
                }
            }
        });
    }
    
    // Start the clock updating interval
    startUpdating() {
        this.updateTimes();
        this.updateInterval = setInterval(() => {
            this.updateTimes();
        }, 1000);
    }
    
    // Stop the clock updating interval
    stopUpdating() {
        if (this.updateInterval) {
            clearInterval(this.updateInterval);
            this.updateInterval = null;
        }
    }
    
    // Show notification to user
    showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.className = `notification notification-${type}`;
        notification.textContent = message;
        
        document.body.appendChild(notification);
        
        setTimeout(() => {
            if (notification.parentNode) {
                notification.parentNode.removeChild(notification);
            }
        }, 3000);
    }
    
    // Add a new city
    addCity() {
        const select = document.getElementById('timezone-select');
        const selectedValue = select.value;
        
        if (!selectedValue) {
            this.showNotification('Please select a timezone', 'error');
            return;
        }
        
        const cityData = JSON.parse(selectedValue);
        
        // Check if city already exists
        const exists = this.cities.some(city => city.timezone === cityData.timezone);
        if (exists) {
            this.showNotification('This city is already added!', 'error');
            return;
        }
        
        this.cities.push(cityData);
        this.saveSettings();
        this.renderClocks();
        this.hideModal();
        this.showNotification(`${cityData.name} added successfully!`, 'success');
    }
    
    // Remove a city
    removeCity(timezone) {
        const city = this.cities.find(c => c.timezone === timezone);
        this.cities = this.cities.filter(c => c.timezone !== timezone);
        this.saveSettings();
        this.renderClocks();
        
        if (city) {
            this.showNotification(`${city.name} removed`, 'info');
        }
    }
    
    // Hide the add city modal
    hideModal() {
        document.getElementById('city-modal').classList.remove('active');
        document.getElementById('timezone-select').value = '';
    }
}

// Initialize the application when page loads
let worldClock;

document.addEventListener('DOMContentLoaded', () => {
    worldClock = new WorldClock();
});

// Optimize performance by pausing updates when page is hidden
document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
        worldClock.stopUpdating();
    } else {
        worldClock.startUpdating();
    }
});

Testing and Deployment

Step 4: Testing Your Widget

๐Ÿงช Testing Checklist
  • Basic functionality: Open index.html in your browser
  • Time updates: Verify times update correctly every second
  • Theme switching: Test light/dark mode transitions
  • Add/remove cities: Test adding and removing different cities
  • Format toggle: Test 12/24 hour format switching
  • Responsive design: Test on mobile, tablet, and desktop
  • Persistence: Refresh page and verify settings are saved

Step 5: Deployment Options

๐Ÿš€ Quick Deployment
  • Local testing: Open index.html directly in your browser
  • GitHub Pages: Push to GitHub and enable Pages in repository settings
  • Netlify: Drag and drop your folder to netlify.com
  • Vercel: Connect your GitHub repository for automatic deployment

Embedding the Widget

To embed your widget in another website, use this code:

<!-- World Clock Widget -->
<div id="world-clock-widget"></div>
<link rel="stylesheet" href="https://your-domain.com/css/style.css">
<script src="https://your-domain.com/js/app.js"></script>

Congratulations! ๐ŸŽ‰

You've successfully built a complete world clock widget! Here's what you've accomplished:

โœ… What You've Built
  • Functional World Clock: Real-time updates for multiple time zones
  • Interactive Interface: Add/remove cities, toggle formats and themes
  • Responsive Design: Works perfectly on all device sizes
  • Data Persistence: Remembers user preferences using localStorage
  • Clean Code: Well-structured, maintainable code
  • No Dependencies: Pure vanilla web technologies

Next Steps

Now that you have a working widget, consider these enhancements:

  • Weather integration: Show weather conditions alongside time
  • Alarm system: Set alarms for different time zones
  • Meeting planner: Find optimal meeting times across zones
  • Custom themes: Add more color schemes
  • Analog clocks: Option to display traditional clock faces
  • Time zone converter: Convert specific times between zones

This project demonstrates core web development concepts including DOM manipulation, CSS Grid, localStorage, and responsive design. You now have a solid foundation to build more complex web applications!

๐Ÿ’ก Key Learning Points
  • JavaScript Classes: Organized code structure with methods and properties
  • Browser APIs: Using Intl.DateTimeFormat for timezone handling
  • CSS Custom Properties: Dynamic theming with CSS variables
  • Local Storage: Persisting user data in the browser
  • Event Handling: Managing user interactions and keyboard shortcuts
  • Responsive Design: Creating layouts that work on all devices