Skip to content
UI ComponentFooterLayoutNew

Mega Footer with CTA, Sitemap, Locale & Live Status

A big, modern footer: animated call-to-action band, sitemap columns rendered from data (accordions on phones), a newsletter field with validation, a pulsing 'All systems operational' status, a live local clock, custom language and currency dropdowns that remember the choice, social icons with tooltips, a back-to-top button with a progress ring and a giant outlined brand wordmark lit by a spotlight that follows the mouse.

  • Published 15 Oct 2026
  • 6 views
Mega Footer with CTA, Sitemap, Locale & Live Status — preview

Overview

Mega Footer with CTA, Sitemap, Locale & Live Status

A footer people actually scroll to — CTA, sitemap, live status, locale pickers and a giant wordmark that lights up under your cursor.


What's Included

  • Call-to-action band with a drifting grid background
  • Sitemap columns from one array — accordions on phones
  • Newsletter input with validation and success message
  • Pulsing system status and a live clock in your time zone
  • Keyboard-friendly language and currency dropdowns (remembered)
  • Giant outlined wordmark with a mouse-following gradient spotlight
  • Back-to-top button with a scroll-progress ring

Dependencies

Already linked inside the component's files — no extra setup needed.


Desktop Preview


HTML

html
1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>Mega CTA Footer | MetaSkillGroup</title>
7    <link rel="stylesheet" href="style.css" />
8  </head>
9  <body>
10    <!-- created by metaskillgroup-team -->
11    <main class="page-content">
12      <p class="hint">↓ Scroll to the footer</p>
13    </main>
14
15    <footer class="footer">
16      <!-- Call to action -->
17      <section class="cta">
18        <div class="cta-grid" aria-hidden="true"></div>
19        <div class="cta-text">
20          <span class="eyebrow">Start today — free for 14 days</span>
21          <h2>Ready to ship your<br />next big idea?</h2>
22        </div>
23        <div class="cta-actions">
24          <a href="#" class="btn primary">Start building →</a>
25          <a href="#" class="btn ghost">Book a demo</a>
26        </div>
27      </section>
28
29      <div class="main">
30        <!-- Brand + newsletter -->
31        <div class="brand-col">
32          <a href="#" class="logo"><span class="logo-mark" aria-hidden="true"></span>Lumen</a>
33          <p class="tagline">The design-to-code platform for teams who care about details.</p>
34
35          <form class="newsletter" id="newsletter" novalidate>
36            <label for="nl-email">Get product updates — one email a month.</label>
37            <div class="nl-row">
38              <input type="email" id="nl-email" placeholder="you@company.com" autocomplete="email" />
39              <button type="submit" aria-label="Subscribe">
40                <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
41              </button>
42            </div>
43            <small class="nl-msg" id="nl-msg" aria-live="polite"></small>
44          </form>
45
46          <a href="#" class="status" id="status">
47            <span class="status-dot" aria-hidden="true"></span>
48            <span>All systems operational</span>
49            <small id="uptime">99.98% uptime</small>
50          </a>
51        </div>
52
53        <!-- Sitemap columns are rendered from script.js -->
54        <nav class="sitemap" id="sitemap" aria-label="Footer"></nav>
55      </div>
56
57      <!-- Bottom bar -->
58      <div class="bottom">
59        <p class="copy">© <span id="year"></span> Lumen Labs Pvt. Ltd. · Made with ♥ in India</p>
60
61        <div class="clock" aria-live="off">
62          <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>
63          <span id="clock">--:--</span> in Bengaluru
64        </div>
65
66        <div class="selects">
67          <div class="select" data-select="lang">
68            <button type="button" class="select-btn" aria-haspopup="listbox" aria-expanded="false">
69              🌐 <span class="select-value"></span>
70            </button>
71            <ul class="select-list" role="listbox" tabindex="-1"></ul>
72          </div>
73          <div class="select" data-select="currency">
74            <button type="button" class="select-btn" aria-haspopup="listbox" aria-expanded="false">
75              <span class="select-value"></span>
76            </button>
77            <ul class="select-list" role="listbox" tabindex="-1"></ul>
78          </div>
79        </div>
80
81        <ul class="socials" id="socials"></ul>
82      </div>
83
84      <!-- Giant wordmark — a spotlight follows the mouse and fills the letters -->
85      <div class="wordmark" id="wordmark" aria-hidden="true">
86        <span class="outline">LUMEN</span>
87        <span class="fill">LUMEN</span>
88      </div>
89
90      <button type="button" class="to-top" id="to-top" aria-label="Back to top">
91        <svg class="to-top-ring" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="21" /></svg>
92        <svg class="to-top-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6" /></svg>
93      </button>
94    </footer>
95
96    <script src="script.js"></script>
97  </body>
98</html>

CSS

css
1/* Importing Google Fonts - Archivo */
2@import url("https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&display=swap");
3
4* {
5  margin: 0;
6  padding: 0;
7  box-sizing: border-box;
8  font-family: "Archivo", sans-serif;
9}
10
11:root {
12  --page: #f4f1ec;
13  --bg: #0c0c10;
14  --panel: #15151b;
15  --line: #24242c;
16  --text: #f5f3ef;
17  --muted: #8e8c96;
18  --accent: #ff6a3d;
19  --accent-2: #ff3d81;
20  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
21}
22
23body {
24  background: var(--page);
25  color: var(--text);
26  overflow-x: hidden;
27}
28
29a {
30  color: inherit;
31  text-decoration: none;
32}
33
34.page-content {
35  display: grid;
36  place-items: center;
37  min-height: 70vh;
38}
39
40.hint {
41  color: #8a8478;
42  font-weight: 600;
43}
44
45/* ---------- Footer shell ---------- */
46.footer {
47  position: relative;
48  margin: 0 16px 16px;
49  padding: 26px 26px 0;
50  border-radius: 32px;
51  background: var(--bg);
52  overflow: hidden;
53  isolation: isolate;
54}
55
56/* ---------- CTA band ---------- */
57.cta {
58  position: relative;
59  display: flex;
60  justify-content: space-between;
61  align-items: flex-end;
62  gap: 30px;
63  flex-wrap: wrap;
64  padding: 56px 48px;
65  border-radius: 24px;
66  background:
67    radial-gradient(circle at 85% 120%, rgba(255, 61, 129, 0.55), transparent 45%),
68    radial-gradient(circle at 10% -20%, rgba(255, 106, 61, 0.5), transparent 45%),
69    #1a1418;
70  overflow: hidden;
71}
72
73.cta-grid {
74  position: absolute;
75  inset: 0;
76  background:
77    linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px) 0 0 / 36px 36px,
78    linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px) 0 0 / 36px 36px;
79  mask: radial-gradient(circle at 70% 50%, #000, transparent 70%);
80  animation: drift 20s linear infinite;
81}
82
83@keyframes drift {
84  to {
85    background-position: 36px 36px, 36px 36px;
86  }
87}
88
89.cta-text,
90.cta-actions {
91  position: relative;
92}
93
94.eyebrow {
95  color: #ffb199;
96  font-size: 0.85rem;
97  font-weight: 700;
98  letter-spacing: 0.04em;
99}
100
101.cta h2 {
102  margin-top: 12px;
103  font-size: clamp(2rem, 5vw, 3.6rem);
104  font-weight: 900;
105  line-height: 1;
106  letter-spacing: -0.04em;
107}
108
109.cta-actions {
110  display: flex;
111  gap: 12px;
112  flex-wrap: wrap;
113}
114
115.btn {
116  padding: 15px 24px;
117  border-radius: 14px;
118  font-weight: 700;
119  transition: transform 0.25s var(--ease), box-shadow 0.25s, background 0.25s;
120}
121
122.btn.primary {
123  background: var(--text);
124  color: var(--bg);
125}
126
127.btn.primary:hover {
128  transform: translateY(-3px);
129  box-shadow: 0 16px 40px -14px rgba(255, 106, 61, 0.9);
130}
131
132.btn.ghost {
133  border: 1px solid rgba(255, 255, 255, 0.25);
134}
135
136.btn.ghost:hover {
137  background: rgba(255, 255, 255, 0.08);
138}
139
140/* ---------- Main grid ---------- */
141.main {
142  display: grid;
143  grid-template-columns: 1.1fr 2fr;
144  gap: 60px;
145  padding: 60px 22px 46px;
146}
147
148.logo {
149  display: inline-flex;
150  align-items: center;
151  gap: 10px;
152  font-size: 1.5rem;
153  font-weight: 900;
154  letter-spacing: -0.03em;
155}
156
157.logo-mark {
158  width: 30px;
159  height: 30px;
160  border-radius: 50% 50% 50% 8px;
161  background: linear-gradient(135deg, var(--accent), var(--accent-2));
162}
163
164.tagline {
165  max-width: 320px;
166  margin: 14px 0 26px;
167  color: var(--muted);
168  line-height: 1.6;
169}
170
171.newsletter label {
172  display: block;
173  margin-bottom: 10px;
174  font-size: 0.9rem;
175  font-weight: 600;
176}
177
178.nl-row {
179  display: flex;
180  max-width: 380px;
181  padding: 5px;
182  border: 1px solid var(--line);
183  border-radius: 14px;
184  background: var(--panel);
185  transition: border-color 0.2s;
186}
187
188.nl-row:focus-within {
189  border-color: var(--accent);
190}
191
192.nl-row input {
193  flex: 1;
194  min-width: 0;
195  padding: 10px 12px;
196  border: 0;
197  background: none;
198  color: var(--text);
199  font-size: 0.95rem;
200  outline: none;
201}
202
203.nl-row button {
204  display: grid;
205  place-items: center;
206  width: 44px;
207  border: 0;
208  border-radius: 10px;
209  background: linear-gradient(135deg, var(--accent), var(--accent-2));
210  color: #fff;
211  cursor: pointer;
212}
213
214.nl-row svg,
215.clock svg,
216.socials svg,
217.to-top-arrow,
218.col-btn svg {
219  width: 18px;
220  height: 18px;
221  fill: none;
222  stroke: currentColor;
223  stroke-width: 2;
224  stroke-linecap: round;
225  stroke-linejoin: round;
226}
227
228.nl-msg {
229  display: block;
230  min-height: 20px;
231  margin-top: 8px;
232  font-size: 0.82rem;
233  font-weight: 600;
234}
235
236.nl-msg.error {
237  color: #ff8a8a;
238}
239
240.nl-msg.ok {
241  color: #5ee6a8;
242}
243
244.status {
245  display: inline-flex;
246  align-items: center;
247  gap: 10px;
248  margin-top: 14px;
249  padding: 9px 14px;
250  border: 1px solid var(--line);
251  border-radius: 999px;
252  font-size: 0.85rem;
253  font-weight: 600;
254  transition: border-color 0.2s;
255}
256
257.status:hover {
258  border-color: #3a3a44;
259}
260
261.status small {
262  color: var(--muted);
263  font-weight: 500;
264}
265
266.status-dot {
267  position: relative;
268  width: 9px;
269  height: 9px;
270  border-radius: 50%;
271  background: #22c55e;
272}
273
274.status-dot::after {
275  content: "";
276  position: absolute;
277  inset: -4px;
278  border-radius: 50%;
279  border: 2px solid #22c55e;
280  animation: pulse 1.8s ease-out infinite;
281}
282
283@keyframes pulse {
284  from {
285    opacity: 0.9;
286    transform: scale(0.6);
287  }
288  to {
289    opacity: 0;
290    transform: scale(1.6);
291  }
292}
293
294/* ---------- Sitemap ---------- */
295.sitemap {
296  display: grid;
297  grid-template-columns: repeat(4, 1fr);
298  gap: 30px;
299}
300
301.col-btn {
302  display: flex;
303  justify-content: space-between;
304  align-items: center;
305  width: 100%;
306  margin-bottom: 16px;
307  padding: 0;
308  border: 0;
309  background: none;
310  color: var(--muted);
311  font-size: 0.8rem;
312  font-weight: 700;
313  letter-spacing: 0.1em;
314  text-transform: uppercase;
315  cursor: default;
316}
317
318.col-btn svg {
319  display: none;
320}
321
322.col-links {
323  display: grid;
324  gap: 12px;
325  list-style: none;
326}
327
328.col-links a {
329  position: relative;
330  display: inline-flex;
331  align-items: center;
332  gap: 8px;
333  color: #d8d5cf;
334  font-size: 0.97rem;
335  transition: color 0.2s, transform 0.25s var(--ease);
336}
337
338.col-links a:hover {
339  color: #fff;
340  transform: translateX(4px);
341}
342
343.col-links .new {
344  padding: 2px 7px;
345  border-radius: 999px;
346  background: rgba(255, 106, 61, 0.18);
347  color: #ffa385;
348  font-size: 0.68rem;
349  font-weight: 800;
350}
351
352/* ---------- Bottom bar ---------- */
353.bottom {
354  display: flex;
355  justify-content: space-between;
356  align-items: center;
357  flex-wrap: wrap;
358  gap: 18px;
359  padding: 22px;
360  border-top: 1px solid var(--line);
361  color: var(--muted);
362  font-size: 0.88rem;
363}
364
365.clock {
366  display: flex;
367  align-items: center;
368  gap: 8px;
369}
370
371.clock span {
372  color: var(--text);
373  font-variant-numeric: tabular-nums;
374  font-weight: 600;
375}
376
377.selects {
378  display: flex;
379  gap: 8px;
380}
381
382.select {
383  position: relative;
384}
385
386.select-btn {
387  display: flex;
388  align-items: center;
389  gap: 6px;
390  padding: 8px 12px;
391  border: 1px solid var(--line);
392  border-radius: 10px;
393  background: var(--panel);
394  color: var(--text);
395  font-size: 0.85rem;
396  font-weight: 600;
397  cursor: pointer;
398}
399
400.select-btn::after {
401  content: "";
402  width: 6px;
403  height: 6px;
404  margin-left: 2px;
405  border-right: 2px solid var(--muted);
406  border-bottom: 2px solid var(--muted);
407  transform: translateY(-2px) rotate(45deg);
408}
409
410.select-list {
411  position: absolute;
412  bottom: calc(100% + 8px);
413  left: 0;
414  z-index: 5;
415  min-width: 170px;
416  padding: 6px;
417  border: 1px solid var(--line);
418  border-radius: 14px;
419  background: var(--panel);
420  box-shadow: 0 20px 50px -20px #000;
421  list-style: none;
422  opacity: 0;
423  visibility: hidden;
424  transform: translateY(6px);
425  transition: opacity 0.2s, transform 0.25s var(--ease), visibility 0.2s;
426}
427
428.select.is-open .select-list {
429  opacity: 1;
430  visibility: visible;
431  transform: none;
432}
433
434.select-list li {
435  display: flex;
436  justify-content: space-between;
437  gap: 12px;
438  padding: 9px 10px;
439  border-radius: 9px;
440  color: #d8d5cf;
441  cursor: pointer;
442}
443
444.select-list li small {
445  color: var(--muted);
446}
447
448.select-list li[aria-selected="true"] {
449  color: #fff;
450  font-weight: 700;
451}
452
453.select-list li[aria-selected="true"]::after {
454  content: "✓";
455  color: var(--accent);
456}
457
458.select-list li.is-focused,
459.select-list li:hover {
460  background: rgba(255, 255, 255, 0.07);
461}
462
463.socials {
464  display: flex;
465  gap: 8px;
466  list-style: none;
467}
468
469.socials a {
470  position: relative;
471  display: grid;
472  place-items: center;
473  width: 38px;
474  height: 38px;
475  border: 1px solid var(--line);
476  border-radius: 12px;
477  color: #d8d5cf;
478  transition: background 0.2s, color 0.2s, transform 0.25s var(--ease);
479}
480
481.socials a:hover,
482.socials a:focus-visible {
483  background: var(--text);
484  color: var(--bg);
485  transform: translateY(-3px);
486}
487
488/* Tooltip */
489.socials a::after {
490  content: attr(data-label);
491  position: absolute;
492  bottom: calc(100% + 8px);
493  left: 50%;
494  padding: 5px 9px;
495  border-radius: 8px;
496  background: var(--text);
497  color: var(--bg);
498  font-size: 0.75rem;
499  font-weight: 700;
500  white-space: nowrap;
501  opacity: 0;
502  transform: translate(-50%, 4px);
503  transition: opacity 0.2s, transform 0.2s;
504  pointer-events: none;
505}
506
507.socials a:hover::after,
508.socials a:focus-visible::after {
509  opacity: 1;
510  transform: translate(-50%, 0);
511}
512
513/* ---------- Giant wordmark ---------- */
514.wordmark {
515  position: relative;
516  margin: 10px -10px -3.2vw;
517  font-size: 23.5vw;
518  font-weight: 900;
519  line-height: 0.85;
520  letter-spacing: -0.06em;
521  text-align: center;
522  user-select: none;
523}
524
525.wordmark span {
526  display: block;
527}
528
529.wordmark .outline {
530  color: transparent;
531  -webkit-text-stroke: 1px #2c2c35;
532}
533
534/* Filled copy, only visible inside the spotlight */
535.wordmark .fill {
536  position: absolute;
537  inset: 0;
538  background: linear-gradient(100deg, var(--accent), var(--accent-2), #8b5cf6);
539  -webkit-background-clip: text;
540  background-clip: text;
541  color: transparent;
542  -webkit-mask: radial-gradient(circle 16vw at var(--mx, 50%) var(--my, 120%), #000 0%, transparent 100%);
543  mask: radial-gradient(circle 16vw at var(--mx, 50%) var(--my, 120%), #000 0%, transparent 100%);
544}
545
546/* ---------- Back to top ---------- */
547.to-top {
548  position: fixed;
549  right: 28px;
550  bottom: 28px;
551  z-index: 20;
552  display: grid;
553  place-items: center;
554  width: 52px;
555  height: 52px;
556  border: 0;
557  border-radius: 50%;
558  background: var(--text);
559  color: var(--bg);
560  cursor: pointer;
561  opacity: 0;
562  visibility: hidden;
563  transform: translateY(16px) scale(0.9);
564  transition: opacity 0.3s, transform 0.35s var(--ease), visibility 0.3s;
565  box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.5);
566}
567
568.to-top.is-visible {
569  opacity: 1;
570  visibility: visible;
571  transform: none;
572}
573
574.to-top:hover .to-top-arrow {
575  transform: translateY(-3px);
576}
577
578.to-top-arrow {
579  transition: transform 0.25s var(--ease);
580}
581
582.to-top-ring {
583  position: absolute;
584  inset: 0;
585  transform: rotate(-90deg);
586}
587
588.to-top-ring circle {
589  fill: none;
590  stroke: var(--accent);
591  stroke-width: 3;
592  stroke-linecap: round;
593  stroke-dasharray: 132;
594  stroke-dashoffset: calc(132 * (1 - var(--p, 0)));
595}
596
597.btn:focus-visible,
598.select-btn:focus-visible,
599.to-top:focus-visible,
600.nl-row button:focus-visible,
601.col-btn:focus-visible {
602  outline: 3px solid rgba(255, 106, 61, 0.6);
603  outline-offset: 3px;
604}
605
606/* ---------- Responsive ---------- */
607@media (max-width: 960px) {
608  .main {
609    grid-template-columns: 1fr;
610    gap: 40px;
611  }
612}
613
614/* Sitemap columns become accordions on phones */
615@media (max-width: 640px) {
616  .footer {
617    margin: 0 8px 8px;
618    padding: 10px 10px 0;
619    border-radius: 24px;
620  }
621
622  .cta {
623    padding: 36px 24px;
624  }
625
626  .main {
627    padding: 40px 12px 30px;
628  }
629
630  .sitemap {
631    grid-template-columns: 1fr;
632    gap: 0;
633  }
634
635  .col {
636    border-bottom: 1px solid var(--line);
637  }
638
639  .col-btn {
640    margin: 0;
641    padding: 18px 0;
642    color: var(--text);
643    font-size: 0.9rem;
644    cursor: pointer;
645  }
646
647  .col-btn svg {
648    display: block;
649    transition: transform 0.3s var(--ease);
650  }
651
652  .col-btn[aria-expanded="true"] svg {
653    transform: rotate(180deg);
654  }
655
656  .col-panel {
657    display: grid;
658    grid-template-rows: 0fr;
659    transition: grid-template-rows 0.35s var(--ease);
660  }
661
662  .col-panel > div {
663    overflow: hidden;
664  }
665
666  .col-btn[aria-expanded="true"] + .col-panel {
667    grid-template-rows: 1fr;
668  }
669
670  .col-links {
671    padding-bottom: 18px;
672  }
673
674  .bottom {
675    flex-direction: column;
676    align-items: flex-start;
677    padding: 22px 12px;
678  }
679
680  .to-top {
681    right: 18px;
682    bottom: 18px;
683  }
684}
685
686@media (prefers-reduced-motion: reduce) {
687  *,
688  *::before,
689  *::after {
690    animation: none !important;
691    transition: none !important;
692  }
693}

JavaScript

javascript
1// ---------- Data (edit freely) ----------
2const sitemap = [
3  { title: "Product", links: ["Features", "Integrations", ["AI Copilot", "New"], "Pricing", "Changelog", "Roadmap"] },
4  { title: "Company", links: ["About", ["Careers", "We're hiring"], "Customers", "Press kit", "Contact"] },
5  { title: "Resources", links: ["Docs", "Guides", "Templates", "Community", "Status"] },
6  { title: "Legal", links: ["Privacy", "Terms", "Cookies", "Security", "DPA"] },
7];
8
9const languages = [
10  { value: "en", label: "English" },
11  { value: "hi", label: "हिन्दी" },
12  { value: "es", label: "Español" },
13  { value: "de", label: "Deutsch" },
14  { value: "ja", label: "日本語" },
15];
16
17const currencies = [
18  { value: "INR", label: "₹ INR", note: "Rupee" },
19  { value: "USD", label: "$ USD", note: "Dollar" },
20  { value: "EUR", label: "€ EUR", note: "Euro" },
21  { value: "GBP", label: "£ GBP", note: "Pound" },
22];
23
24const socials = [
25  { label: "GitHub", path: '<path d="M9 19c-4 1.5-4-2-6-2.5M15 21v-3.5a3 3 0 0 0-.9-2.3c3-.3 6-1.5 6-6.7a5.2 5.2 0 0 0-1.4-3.6 4.9 4.9 0 0 0-.1-3.6s-1.1-.3-3.6 1.4a12.4 12.4 0 0 0-6.5 0C6 1.1 4.9 1.4 4.9 1.4a4.9 4.9 0 0 0-.1 3.6A5.2 5.2 0 0 0 3.4 8.6c0 5.2 3 6.4 6 6.7a3 3 0 0 0-.9 2.3V21"/>' },
26  { label: "LinkedIn", path: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2z"/><circle cx="4" cy="4" r="2"/>' },
27  { label: "YouTube", path: '<rect x="2" y="5" width="20" height="14" rx="4"/><path d="M10 9l5 3-5 3z"/>' },
28  { label: "Instagram", path: '<rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".5"/>' },
29];
30
31const TIME_ZONE = "Asia/Kolkata";
32
33const store = {
34  get: (k) => { try { return localStorage.getItem(k); } catch { return null; } },
35  set: (k, v) => { try { localStorage.setItem(k, v); } catch { /* storage blocked */ } },
36};
37const phone = window.matchMedia("(max-width: 640px)");
38
39// ---------- Sitemap (columns on desktop, accordions on phones) ----------
40const sitemapEl = document.getElementById("sitemap");
41sitemapEl.innerHTML = sitemap
42  .map(
43    (col, i) => `<div class="col">
44      <button type="button" class="col-btn" aria-expanded="true" aria-controls="col-${i}">${col.title}
45        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
46      <div class="col-panel" id="col-${i}"><div><ul class="col-links">
47        ${col.links
48          .map((l) => (Array.isArray(l) ? `<li><a href="#">${l[0]} <span class="new">${l[1]}</span></a></li>` : `<li><a href="#">${l}</a></li>`))
49          .join("")}
50      </ul></div></div>
51    </div>`,
52  )
53  .join("");
54
55const colButtons = [...sitemapEl.querySelectorAll(".col-btn")];
56const syncAccordions = () => colButtons.forEach((b) => b.setAttribute("aria-expanded", String(!phone.matches)));
57colButtons.forEach((btn) =>
58  btn.addEventListener("click", () => {
59    if (!phone.matches) return; // plain headings on desktop
60    btn.setAttribute("aria-expanded", String(btn.getAttribute("aria-expanded") !== "true"));
61  }),
62);
63phone.addEventListener("change", syncAccordions);
64syncAccordions();
65
66// ---------- Socials ----------
67document.getElementById("socials").innerHTML = socials
68  .map((s) => `<li><a href="#" aria-label="${s.label}" data-label="${s.label}"><svg viewBox="0 0 24 24" aria-hidden="true">${s.path}</svg></a></li>`)
69  .join("");
70
71// ---------- Newsletter ----------
72const form = document.getElementById("newsletter");
73const email = document.getElementById("nl-email");
74const msg = document.getElementById("nl-msg");
75form.addEventListener("submit", (e) => {
76  e.preventDefault();
77  const ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.value.trim());
78  msg.className = `nl-msg ${ok ? "ok" : "error"}`;
79  msg.textContent = ok ? "You're subscribed — see you next month 🎉" : "Please enter a valid email address.";
80  if (ok) email.value = ""; // replace with your API call
81});
82
83// ---------- Custom selects (language + currency, remembered) ----------
84const setupSelect = (root, options, key) => {
85  const btn = root.querySelector(".select-btn");
86  const list = root.querySelector(".select-list");
87  const valueEl = root.querySelector(".select-value");
88  let value = store.get(key) || options[0].value;
89  let focus = 0;
90
91  const render = () => {
92    const current = options.find((o) => o.value === value) ?? options[0];
93    valueEl.textContent = current.label;
94    list.innerHTML = options
95      .map((o, i) => `<li role="option" data-value="${o.value}" aria-selected="${o.value === value}" class="${i === focus ? "is-focused" : ""}">${o.label}${o.note ? ` <small>${o.note}</small>` : ""}</li>`)
96      .join("");
97  };
98  const open = (yes) => {
99    document.querySelectorAll(".select.is-open").forEach((s) => s !== root && s.classList.remove("is-open"));
100    root.classList.toggle("is-open", yes);
101    btn.setAttribute("aria-expanded", yes);
102    if (yes) {
103      focus = Math.max(0, options.findIndex((o) => o.value === value));
104      render();
105      list.focus();
106    }
107  };
108  const choose = (v) => {
109    value = v;
110    store.set(key, v);
111    render();
112    open(false);
113    btn.focus();
114  };
115
116  btn.addEventListener("click", (e) => {
117    e.stopPropagation();
118    open(!root.classList.contains("is-open"));
119  });
120  list.addEventListener("click", (e) => {
121    e.stopPropagation();
122    const li = e.target.closest("li");
123    if (li) choose(li.dataset.value);
124  });
125  list.addEventListener("keydown", (e) => {
126    if (e.key === "ArrowDown") focus = (focus + 1) % options.length;
127    else if (e.key === "ArrowUp") focus = (focus - 1 + options.length) % options.length;
128    else if (e.key === "Enter" || e.key === " ") return e.preventDefault(), choose(options[focus].value);
129    else if (e.key === "Escape") return open(false), btn.focus();
130    else return;
131    e.preventDefault();
132    render();
133  });
134  render();
135};
136
137setupSelect(document.querySelector('[data-select="lang"]'), languages, "footer-lang");
138setupSelect(document.querySelector('[data-select="currency"]'), currencies, "footer-currency");
139document.addEventListener("click", () => document.querySelectorAll(".select.is-open").forEach((s) => s.classList.remove("is-open")));
140
141// ---------- Live clock + year ----------
142const clock = document.getElementById("clock");
143const fmt = new Intl.DateTimeFormat("en-IN", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true, timeZone: TIME_ZONE });
144const tick = () => (clock.textContent = fmt.format(new Date()).toUpperCase());
145tick();
146setInterval(tick, 1000);
147document.getElementById("year").textContent = new Date().getFullYear();
148
149// ---------- Wordmark spotlight ----------
150const wordmark = document.getElementById("wordmark");
151const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
152let lastPointer = 0;
153
154const footer = document.querySelector(".footer");
155footer.addEventListener("pointermove", (e) => {
156  const r = wordmark.getBoundingClientRect();
157  lastPointer = performance.now();
158  wordmark.style.setProperty("--mx", `${e.clientX - r.left}px`);
159  wordmark.style.setProperty("--my", `${e.clientY - r.top}px`);
160});
161
162// When nobody is pointing, the spotlight drifts across the letters by itself
163const drift = (now) => {
164  if (now - lastPointer > 1500) {
165    const r = wordmark.getBoundingClientRect();
166    const t = now / 1000;
167    wordmark.style.setProperty("--mx", `${r.width * (0.5 + 0.42 * Math.sin(t * 0.7))}px`);
168    wordmark.style.setProperty("--my", `${r.height * (0.55 + 0.15 * Math.cos(t * 1.3))}px`);
169  }
170  requestAnimationFrame(drift);
171};
172if (!reduceMotion) requestAnimationFrame(drift);
173
174// ---------- Back to top with progress ring ----------
175const toTop = document.getElementById("to-top");
176const onScroll = () => {
177  const max = document.documentElement.scrollHeight - innerHeight;
178  toTop.style.setProperty("--p", max > 0 ? (scrollY / max).toFixed(3) : 0);
179  toTop.classList.toggle("is-visible", scrollY > 300);
180};
181window.addEventListener("scroll", onScroll, { passive: true });
182toTop.addEventListener("click", () => window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" }));
183onScroll();

Built by MetaSkillGroup — copy it, use it, make it yours.

Features

  • Call-to-action band with a drifting grid background

  • Sitemap columns from one array — accordions on phones

  • Newsletter input with validation and success message

  • Pulsing system status and a live clock in your time zone

  • Keyboard-friendly language and currency dropdowns (remembered)

  • Giant outlined wordmark with a mouse-following gradient spotlight

  • Back-to-top button with a scroll-progress ring

Built With

🌐

HTML5

🎨

CSS3

⚡

JavaScript

Compatibility

Browsers
ChromeEdgeFirefoxSafariOperaBraveArcVivaldi
Devices
DesktopTabletMobile
Frameworks
Vanilla JSHTML/CSS

❓ FAQs

Edit the sitemap array at the top of script.js. Each column has a title and links; a link written as ["AI Copilot", "New"] shows a small badge.

Asia/Kolkata by default. Change TIME_ZONE in script.js to any IANA zone such as Europe/London or America/New_York, and update the city text in index.html.

Yes — with no mouse, the spotlight drifts across the letters on its own. It's skipped entirely for users who prefer reduced motion.

MetaSkillGroup

Written by

MetaSkillGroup

October 15, 2026

Need it built for you?

Built to the same standard as our client work.

If nothing here fits exactly, tell us what you need — our core developers build it. Requirements call first, a written SRS before any code, and a price agreed up front.

Mega Footer with Newsletter & Language Switcher | MetaSkillGroup