Skip to content
UI ComponentHeaderNavigationNew

SaaS Header with Mega Menu & ⌘K Command Palette

A production-ready SaaS header: dismissible announcement bar, hover-intent mega menu with a featured card, list dropdowns, a highlight that glides between nav links, a ⌘K / Ctrl K command palette with fuzzy search, grouped results, keyboard navigation and recent items, plus a header that turns solid on scroll, hides while you read and comes back when you scroll up. On phones it becomes a full drawer with accordion sections.

  • Published 15 Oct 2026
  • 12 views
SaaS Header with Mega Menu & ⌘K Command Palette — preview

Overview

SaaS Header with Mega Menu & ⌘K Command Palette

The header your SaaS deserves — mega menu, a ⌘K command palette and smart hide-on-scroll, all in plain HTML, CSS and JavaScript.


What's Included

  • Mega menu with icon grid and a featured card, plus simple list dropdowns
  • Hover intent (no flicker), click and full keyboard support (↓, ↑, Esc)
  • Highlight pill that glides between nav links
  • ⌘K / Ctrl K command palette with fuzzy search, highlighted matches and recent items
  • Turns solid + blurred after scrolling, hides on scroll down, returns on scroll up
  • Dismissible announcement bar (remembered)
  • Mobile drawer with accordion sections built from the desktop menus

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>SaaS Header with Command Palette | MetaSkillGroup</title>
7    <link rel="stylesheet" href="style.css" />
8  </head>
9  <body>
10    <!-- created by metaskillgroup-team -->
11
12    <!-- Announcement bar (dismiss is remembered) -->
13    <div class="announce" id="announce">
14      <p><span class="pill">New</span> Flowbase 3.0 is here — AI workflows, 2× faster builds. <a href="#">See what's new →</a></p>
15      <button type="button" class="announce-close" aria-label="Dismiss announcement">✕</button>
16    </div>
17
18    <header class="header" id="header">
19      <div class="header-inner">
20        <a href="#" class="logo" aria-label="Flowbase home">
21          <span class="logo-mark" aria-hidden="true"></span>
22          Flowbase
23        </a>
24
25        <nav class="nav" aria-label="Main">
26          <span class="nav-hover" aria-hidden="true"></span>
27          <ul class="nav-list">
28            <li class="has-menu">
29              <button type="button" class="nav-link" aria-expanded="false" aria-controls="menu-product">
30                Product <svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
31              </button>
32              <div class="menu mega" id="menu-product">
33                <div class="mega-grid" id="mega-grid"></div>
34                <a href="#" class="mega-feature">
35                  <span class="mf-badge">Just launched</span>
36                  <strong>AI Workflow Builder</strong>
37                  <span>Describe what you need — Flowbase builds the automation for you.</span>
38                  <span class="mf-art" aria-hidden="true"><i></i><i></i><i></i></span>
39                </a>
40              </div>
41            </li>
42            <li class="has-menu">
43              <button type="button" class="nav-link" aria-expanded="false" aria-controls="menu-solutions">
44                Solutions <svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
45              </button>
46              <div class="menu list" id="menu-solutions">
47                <a href="#"><b>Startups</b><span>Move fast without breaking things</span></a>
48                <a href="#"><b>Agencies</b><span>Manage every client in one place</span></a>
49                <a href="#"><b>Enterprise</b><span>Security, SSO and audit logs</span></a>
50                <a href="#"><b>Education</b><span>Free for students and teachers</span></a>
51              </div>
52            </li>
53            <li class="has-menu">
54              <button type="button" class="nav-link" aria-expanded="false" aria-controls="menu-resources">
55                Resources <svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
56              </button>
57              <div class="menu list" id="menu-resources">
58                <a href="#"><b>Docs</b><span>Guides and API reference</span></a>
59                <a href="#"><b>Blog</b><span>Product news and tutorials</span></a>
60                <a href="#"><b>Community</b><span>Join 20k builders on Discord</span></a>
61                <a href="#"><b>Changelog</b><span>Every update, every week</span></a>
62              </div>
63            </li>
64            <li><a href="#" class="nav-link">Pricing</a></li>
65          </ul>
66        </nav>
67
68        <div class="actions">
69          <button type="button" class="search-btn" id="open-palette" aria-label="Search (Ctrl K)">
70            <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg>
71            <span>Search…</span>
72            <kbd id="kbd-hint">Ctrl K</kbd>
73          </button>
74          <a href="#" class="login">Log in</a>
75          <a href="#" class="cta">Get started</a>
76          <button type="button" class="burger" id="burger" aria-label="Open menu" aria-expanded="false" aria-controls="drawer">
77            <span></span><span></span><span></span>
78          </button>
79        </div>
80      </div>
81    </header>
82
83    <!-- Mobile drawer -->
84    <div class="drawer" id="drawer" aria-hidden="true">
85      <nav aria-label="Mobile">
86        <button type="button" class="drawer-search" data-open-palette>
87          <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg>
88          Search docs, pages, actions…
89        </button>
90        <div class="acc" id="drawer-acc"></div>
91        <a href="#" class="drawer-link">Pricing</a>
92        <div class="drawer-cta">
93          <a href="#" class="login">Log in</a>
94          <a href="#" class="cta">Get started free</a>
95        </div>
96      </nav>
97    </div>
98
99    <!-- Command palette -->
100    <dialog class="palette" id="palette" aria-label="Command palette">
101      <div class="palette-box">
102        <div class="palette-input">
103          <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg>
104          <input type="text" id="palette-input" placeholder="Type a command or search…" autocomplete="off" role="combobox" aria-expanded="true" aria-controls="palette-list" />
105          <kbd>Esc</kbd>
106        </div>
107        <ul class="palette-list" id="palette-list" role="listbox"></ul>
108        <div class="palette-foot">
109          <span><kbd>↑</kbd><kbd>↓</kbd> to navigate</span>
110          <span><kbd>↵</kbd> to open</span>
111          <span><kbd>Esc</kbd> to close</span>
112        </div>
113      </div>
114    </dialog>
115
116    <div class="toast" id="toast" role="status" aria-live="polite"></div>
117
118    <!-- Demo page content so you can see the header react to scrolling -->
119    <main class="demo">
120      <section class="hero">
121        <span class="eyebrow">Header component</span>
122        <h1>Build workflows<br />your team <em>loves.</em></h1>
123        <p>Scroll down — the header turns solid, hides while you read and comes back the moment you scroll up. Press <kbd>Ctrl</kbd> <kbd>K</kbd> to open the command palette.</p>
124      </section>
125      <section class="blocks">
126        <div></div><div></div><div></div><div></div><div></div><div></div>
127      </section>
128    </main>
129
130    <script src="script.js"></script>
131  </body>
132</html>

CSS

css
1/* Importing Google Fonts - Instrument Sans + Instrument Serif */
2@import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@1&display=swap");
3
4* {
5  margin: 0;
6  padding: 0;
7  box-sizing: border-box;
8  font-family: "Instrument Sans", sans-serif;
9}
10
11:root {
12  --bg: #f7f7f4;
13  --surface: #ffffff;
14  --text: #15161c;
15  --muted: #6a6d7c;
16  --border: #e6e6e0;
17  --accent: #2f5bff;
18  --accent-soft: #eaefff;
19  --header-h: 72px;
20  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
21}
22
23body {
24  background: var(--bg);
25  color: var(--text);
26}
27
28body.no-scroll {
29  overflow: hidden;
30}
31
32a {
33  color: inherit;
34  text-decoration: none;
35}
36
37kbd {
38  display: inline-block;
39  min-width: 22px;
40  padding: 1px 6px;
41  border: 1px solid var(--border);
42  border-bottom-width: 2px;
43  border-radius: 6px;
44  background: var(--surface);
45  color: var(--muted);
46  font-family: inherit;
47  font-size: 0.72rem;
48  font-weight: 600;
49  text-align: center;
50}
51
52/* ---------- Announcement bar ---------- */
53.announce {
54  position: relative;
55  display: flex;
56  justify-content: center;
57  align-items: center;
58  padding: 10px 48px;
59  background: var(--text);
60  color: #e9eaf2;
61  font-size: 0.88rem;
62  text-align: center;
63}
64
65.announce .pill {
66  margin-right: 8px;
67  padding: 2px 8px;
68  border-radius: 999px;
69  background: var(--accent);
70  color: #fff;
71  font-size: 0.72rem;
72  font-weight: 700;
73}
74
75.announce a {
76  margin-left: 6px;
77  color: #9db2ff;
78  font-weight: 600;
79}
80
81.announce-close {
82  position: absolute;
83  right: 14px;
84  border: 0;
85  background: none;
86  color: #9a9cb0;
87  font-size: 0.85rem;
88  cursor: pointer;
89}
90
91.announce.is-hidden {
92  display: none;
93}
94
95/* ---------- Header ---------- */
96.header {
97  position: sticky;
98  top: 0;
99  z-index: 50;
100  height: var(--header-h);
101  transition: transform 0.4s var(--ease), background 0.3s, box-shadow 0.3s, border-color 0.3s;
102  border-bottom: 1px solid transparent;
103}
104
105/* After scrolling: solid, blurred, with a hairline */
106.header.is-scrolled {
107  background: rgba(255, 255, 255, 0.82);
108  backdrop-filter: blur(14px) saturate(1.4);
109  border-bottom-color: var(--border);
110  box-shadow: 0 10px 30px -20px rgba(21, 22, 28, 0.25);
111}
112
113/* Scrolling down hides the header, scrolling up brings it back */
114.header.is-hidden {
115  transform: translateY(-100%);
116}
117
118.header-inner {
119  display: flex;
120  align-items: center;
121  gap: 28px;
122  max-width: 1200px;
123  height: 100%;
124  margin: 0 auto;
125  padding: 0 24px;
126}
127
128.logo {
129  display: flex;
130  align-items: center;
131  gap: 10px;
132  font-size: 1.2rem;
133  font-weight: 700;
134  letter-spacing: -0.02em;
135}
136
137.logo-mark {
138  width: 28px;
139  height: 28px;
140  border-radius: 9px;
141  background:
142    radial-gradient(circle at 30% 30%, #8ea6ff, transparent 60%),
143    linear-gradient(135deg, var(--accent), #7c3aed);
144  box-shadow: inset 0 -6px 10px rgba(0, 0, 0, 0.2);
145}
146
147/* ---------- Desktop nav ---------- */
148.nav {
149  position: relative;
150  flex: 1;
151}
152
153.nav-list {
154  display: flex;
155  gap: 4px;
156  list-style: none;
157}
158
159.nav-list > li {
160  position: relative;
161}
162
163.nav-link {
164  position: relative;
165  z-index: 1;
166  display: inline-flex;
167  align-items: center;
168  gap: 4px;
169  padding: 9px 14px;
170  border: 0;
171  border-radius: 10px;
172  background: none;
173  color: var(--text);
174  font-size: 0.95rem;
175  font-weight: 500;
176  cursor: pointer;
177}
178
179.nav-link:focus-visible,
180.search-btn:focus-visible,
181.cta:focus-visible,
182.burger:focus-visible {
183  outline: 3px solid rgba(47, 91, 255, 0.35);
184  outline-offset: 2px;
185}
186
187.chev {
188  width: 16px;
189  height: 16px;
190  fill: none;
191  stroke: currentColor;
192  stroke-width: 2;
193  transition: transform 0.3s var(--ease);
194}
195
196.nav-link[aria-expanded="true"] .chev {
197  transform: rotate(180deg);
198}
199
200/* Highlight that glides between hovered nav items (moved by script.js) */
201.nav-hover {
202  position: absolute;
203  top: 0;
204  left: 0;
205  height: 100%;
206  width: var(--w, 0);
207  border-radius: 10px;
208  background: rgba(21, 22, 28, 0.06);
209  opacity: 0;
210  transform: translateX(var(--x, 0));
211  transition: transform 0.3s var(--ease), width 0.3s var(--ease), opacity 0.2s;
212  pointer-events: none;
213}
214
215.nav-hover.is-on {
216  opacity: 1;
217}
218
219/* Dropdowns */
220.menu {
221  position: absolute;
222  top: calc(100% + 12px);
223  left: 0;
224  padding: 10px;
225  border: 1px solid var(--border);
226  border-radius: 18px;
227  background: var(--surface);
228  box-shadow: 0 30px 60px -24px rgba(21, 22, 28, 0.3);
229  opacity: 0;
230  visibility: hidden;
231  transform: translateY(-6px) scale(0.98);
232  transform-origin: top left;
233  transition: opacity 0.2s, transform 0.25s var(--ease), visibility 0.2s;
234}
235
236/* Invisible bridge so the menu doesn't close on the way down */
237.menu::before {
238  content: "";
239  position: absolute;
240  top: -14px;
241  left: 0;
242  right: 0;
243  height: 14px;
244}
245
246.has-menu.is-open .menu {
247  opacity: 1;
248  visibility: visible;
249  transform: none;
250}
251
252.menu.list {
253  display: grid;
254  min-width: 280px;
255}
256
257.menu.list a,
258.mega-item {
259  display: grid;
260  gap: 2px;
261  padding: 10px 12px;
262  border-radius: 12px;
263  transition: background 0.2s;
264}
265
266.menu.list a:hover,
267.menu.list a:focus-visible,
268.mega-item:hover,
269.mega-item:focus-visible {
270  background: var(--bg);
271  outline: none;
272}
273
274.menu b {
275  font-size: 0.93rem;
276  font-weight: 600;
277}
278
279.menu span {
280  color: var(--muted);
281  font-size: 0.83rem;
282}
283
284.menu.mega {
285  display: grid;
286  grid-template-columns: 1fr 260px;
287  gap: 10px;
288  width: 760px;
289}
290
291.mega-grid {
292  display: grid;
293  grid-template-columns: 1fr 1fr;
294}
295
296.mega-item {
297  grid-template-columns: 40px 1fr;
298  column-gap: 12px;
299  align-items: center;
300}
301
302.mega-item .mi-icon {
303  grid-row: span 2;
304  display: grid;
305  place-items: center;
306  width: 40px;
307  height: 40px;
308  border-radius: 12px;
309  background: var(--accent-soft);
310  color: var(--accent);
311}
312
313.mi-icon svg {
314  width: 20px;
315  height: 20px;
316  fill: none;
317  stroke: currentColor;
318  stroke-width: 2;
319  stroke-linecap: round;
320  stroke-linejoin: round;
321}
322
323.mega-feature {
324  position: relative;
325  display: flex;
326  flex-direction: column;
327  gap: 6px;
328  padding: 20px;
329  border-radius: 14px;
330  background: linear-gradient(160deg, #1d2240, #2f2a6b);
331  color: #fff;
332  overflow: hidden;
333}
334
335.mega-feature span {
336  color: #b8bde0;
337}
338
339.mega-feature strong {
340  font-size: 1.05rem;
341}
342
343.mf-badge {
344  align-self: flex-start;
345  padding: 3px 9px;
346  border-radius: 999px;
347  background: rgba(255, 255, 255, 0.14);
348  color: #fff !important;
349  font-size: 0.72rem !important;
350  font-weight: 700;
351}
352
353.mf-art {
354  display: grid;
355  gap: 6px;
356  margin-top: auto;
357  padding-top: 18px;
358}
359
360.mf-art i {
361  height: 10px;
362  border-radius: 6px;
363  background: linear-gradient(90deg, #6d8bff, #a78bfa);
364  animation: grow 2.4s var(--ease) infinite alternate;
365}
366
367.mf-art i:nth-child(2) {
368  width: 70%;
369  animation-delay: 0.3s;
370}
371
372.mf-art i:nth-child(3) {
373  width: 45%;
374  animation-delay: 0.6s;
375}
376
377@keyframes grow {
378  from {
379    transform: scaleX(0.4);
380    transform-origin: left;
381  }
382}
383
384/* ---------- Right side ---------- */
385.actions {
386  display: flex;
387  align-items: center;
388  gap: 10px;
389}
390
391.search-btn {
392  display: flex;
393  align-items: center;
394  gap: 8px;
395  width: 210px;
396  padding: 8px 10px;
397  border: 1px solid var(--border);
398  border-radius: 12px;
399  background: var(--surface);
400  color: var(--muted);
401  font-size: 0.9rem;
402  cursor: pointer;
403  transition: border-color 0.2s;
404}
405
406.search-btn:hover {
407  border-color: #c9cad1;
408}
409
410.search-btn svg,
411.drawer-search svg,
412.palette-input svg {
413  width: 18px;
414  height: 18px;
415  flex-shrink: 0;
416  fill: none;
417  stroke: currentColor;
418  stroke-width: 2;
419  stroke-linecap: round;
420}
421
422.search-btn span {
423  flex: 1;
424  text-align: left;
425}
426
427.login {
428  padding: 9px 12px;
429  font-size: 0.95rem;
430  font-weight: 500;
431}
432
433.cta {
434  padding: 10px 16px;
435  border-radius: 12px;
436  background: var(--text);
437  color: #fff;
438  font-size: 0.93rem;
439  font-weight: 600;
440  white-space: nowrap;
441  transition: transform 0.2s, background 0.2s;
442}
443
444.cta:hover {
445  background: var(--accent);
446  transform: translateY(-1px);
447}
448
449/* Hamburger → X */
450.burger {
451  display: none;
452  position: relative;
453  width: 42px;
454  height: 42px;
455  border: 1px solid var(--border);
456  border-radius: 12px;
457  background: var(--surface);
458  cursor: pointer;
459}
460
461.burger span {
462  position: absolute;
463  left: 11px;
464  width: 18px;
465  height: 2px;
466  border-radius: 2px;
467  background: var(--text);
468  transition: transform 0.35s var(--ease), opacity 0.2s;
469}
470
471.burger span:nth-child(1) { top: 14px; }
472.burger span:nth-child(2) { top: 20px; }
473.burger span:nth-child(3) { top: 26px; }
474
475.burger[aria-expanded="true"] span:nth-child(1) {
476  transform: translateY(6px) rotate(45deg);
477}
478
479.burger[aria-expanded="true"] span:nth-child(2) {
480  opacity: 0;
481}
482
483.burger[aria-expanded="true"] span:nth-child(3) {
484  transform: translateY(-6px) rotate(-45deg);
485}
486
487/* ---------- Mobile drawer ---------- */
488.drawer {
489  position: fixed;
490  inset: var(--header-h) 0 0 0;
491  z-index: 40;
492  padding: 18px 20px 30px;
493  background: var(--surface);
494  overflow-y: auto;
495  opacity: 0;
496  visibility: hidden;
497  transform: translateY(-10px);
498  transition: opacity 0.25s, transform 0.3s var(--ease), visibility 0.25s;
499}
500
501.drawer.is-open {
502  opacity: 1;
503  visibility: visible;
504  transform: none;
505}
506
507.drawer-search {
508  display: flex;
509  align-items: center;
510  gap: 10px;
511  width: 100%;
512  margin-bottom: 14px;
513  padding: 13px 14px;
514  border: 1px solid var(--border);
515  border-radius: 12px;
516  background: var(--bg);
517  color: var(--muted);
518  font-size: 0.95rem;
519  cursor: pointer;
520}
521
522.acc-btn,
523.drawer-link {
524  display: flex;
525  justify-content: space-between;
526  align-items: center;
527  width: 100%;
528  padding: 15px 4px;
529  border: 0;
530  border-bottom: 1px solid var(--border);
531  background: none;
532  color: var(--text);
533  font-size: 1.05rem;
534  font-weight: 600;
535  cursor: pointer;
536}
537
538.acc-panel {
539  display: grid;
540  grid-template-rows: 0fr;
541  transition: grid-template-rows 0.35s var(--ease);
542}
543
544.acc-panel > div {
545  overflow: hidden;
546}
547
548.acc-btn[aria-expanded="true"] + .acc-panel {
549  grid-template-rows: 1fr;
550}
551
552.acc-panel a {
553  display: block;
554  padding: 10px 12px;
555  color: var(--muted);
556}
557
558.drawer-cta {
559  display: grid;
560  gap: 10px;
561  margin-top: 24px;
562  text-align: center;
563}
564
565.drawer-cta .login {
566  border: 1px solid var(--border);
567  border-radius: 12px;
568}
569
570/* ---------- Command palette ---------- */
571.palette {
572  width: min(620px, calc(100% - 32px));
573  margin: 12vh auto auto;
574  padding: 0;
575  border: 0;
576  border-radius: 20px;
577  background: transparent;
578}
579
580.palette::backdrop {
581  background: rgba(21, 22, 28, 0.4);
582  backdrop-filter: blur(4px);
583}
584
585.palette[open] .palette-box {
586  animation: pop 0.22s var(--ease);
587}
588
589@keyframes pop {
590  from {
591    opacity: 0;
592    transform: scale(0.97) translateY(-6px);
593  }
594}
595
596.palette-box {
597  border: 1px solid var(--border);
598  border-radius: 20px;
599  background: var(--surface);
600  box-shadow: 0 40px 100px -30px rgba(21, 22, 28, 0.5);
601  overflow: hidden;
602}
603
604.palette-input {
605  display: flex;
606  align-items: center;
607  gap: 12px;
608  padding: 16px 18px;
609  border-bottom: 1px solid var(--border);
610  color: var(--muted);
611}
612
613.palette-input input {
614  flex: 1;
615  min-width: 0;
616  border: 0;
617  background: none;
618  color: var(--text);
619  font-size: 1.05rem;
620  outline: none;
621}
622
623.palette-list {
624  max-height: min(380px, 50vh);
625  padding: 8px;
626  overflow-y: auto;
627  list-style: none;
628}
629
630.palette-group {
631  padding: 10px 10px 6px;
632  color: var(--muted);
633  font-size: 0.74rem;
634  font-weight: 600;
635  letter-spacing: 0.06em;
636  text-transform: uppercase;
637}
638
639.palette-item {
640  display: flex;
641  align-items: center;
642  gap: 12px;
643  padding: 10px 12px;
644  border-radius: 12px;
645  font-size: 0.95rem;
646  cursor: pointer;
647}
648
649.palette-item .pi-icon {
650  display: grid;
651  place-items: center;
652  width: 30px;
653  height: 30px;
654  border-radius: 9px;
655  background: var(--bg);
656  font-size: 0.95rem;
657}
658
659.palette-item small {
660  margin-left: auto;
661  color: var(--muted);
662  font-size: 0.78rem;
663}
664
665.palette-item mark {
666  background: none;
667  color: var(--accent);
668  font-weight: 700;
669}
670
671.palette-item[aria-selected="true"] {
672  background: var(--accent-soft);
673}
674
675.palette-item[aria-selected="true"] .pi-icon {
676  background: var(--surface);
677}
678
679.palette-empty {
680  padding: 30px;
681  color: var(--muted);
682  text-align: center;
683}
684
685.palette-foot {
686  display: flex;
687  gap: 18px;
688  padding: 10px 16px;
689  border-top: 1px solid var(--border);
690  background: var(--bg);
691  color: var(--muted);
692  font-size: 0.78rem;
693}
694
695.palette-foot kbd {
696  margin-right: 3px;
697}
698
699/* ---------- Toast ---------- */
700.toast {
701  position: fixed;
702  left: 50%;
703  bottom: 24px;
704  z-index: 100;
705  padding: 12px 18px;
706  border-radius: 12px;
707  background: var(--text);
708  color: #fff;
709  font-size: 0.92rem;
710  opacity: 0;
711  transform: translate(-50%, 16px);
712  transition: opacity 0.3s, transform 0.3s var(--ease);
713  pointer-events: none;
714}
715
716.toast.show {
717  opacity: 1;
718  transform: translate(-50%, 0);
719}
720
721/* ---------- Demo page ---------- */
722.demo {
723  max-width: 1200px;
724  margin: 0 auto;
725  padding: 80px 24px 120px;
726}
727
728.hero {
729  max-width: 760px;
730}
731
732.eyebrow {
733  color: var(--accent);
734  font-size: 0.82rem;
735  font-weight: 700;
736  letter-spacing: 0.14em;
737  text-transform: uppercase;
738}
739
740.hero h1 {
741  margin: 16px 0;
742  font-size: clamp(2.6rem, 7vw, 5rem);
743  font-weight: 700;
744  line-height: 1;
745  letter-spacing: -0.04em;
746}
747
748.hero h1 em {
749  font-family: "Instrument Serif", serif;
750  font-weight: 400;
751  color: var(--accent);
752}
753
754.hero p {
755  max-width: 560px;
756  color: var(--muted);
757  font-size: 1.1rem;
758  line-height: 1.65;
759}
760
761.blocks {
762  display: grid;
763  grid-template-columns: repeat(3, 1fr);
764  gap: 20px;
765  margin-top: 70px;
766}
767
768.blocks div {
769  height: 260px;
770  border: 1px solid var(--border);
771  border-radius: 22px;
772  background: linear-gradient(160deg, var(--surface), #f0f0ea);
773}
774
775.blocks div:nth-child(1),
776.blocks div:nth-child(6) {
777  grid-column: span 2;
778  background: linear-gradient(160deg, #e9eeff, #f4ecff);
779}
780
781/* ---------- Responsive ---------- */
782@media (max-width: 1100px) {
783  .search-btn {
784    width: auto;
785  }
786
787  .search-btn span,
788  .search-btn kbd {
789    display: none;
790  }
791
792  .menu.mega {
793    width: 620px;
794    grid-template-columns: 1fr;
795  }
796
797  .mega-feature {
798    display: none;
799  }
800}
801
802@media (max-width: 900px) {
803  .nav,
804  .actions > .login,
805  .actions > .cta {
806    display: none;
807  }
808
809  .header-inner {
810    justify-content: space-between;
811  }
812
813  .burger {
814    display: block;
815  }
816
817  .blocks {
818    grid-template-columns: 1fr;
819  }
820
821  .blocks div:nth-child(1),
822  .blocks div:nth-child(6) {
823    grid-column: auto;
824  }
825
826  .announce {
827    padding: 10px 40px 10px 16px;
828    font-size: 0.8rem;
829  }
830
831  .palette-foot {
832    display: none;
833  }
834}
835
836@media (prefers-reduced-motion: reduce) {
837  *,
838  *::before,
839  *::after {
840    animation: none !important;
841    transition: none !important;
842  }
843}

JavaScript

javascript
1// ---------- Data (edit freely) ----------
2const ICON = {
3  bolt: '<path d="M13 3L4 14h7l-1 7 9-11h-7z"/>',
4  plug: '<path d="M9 7V3M15 7V3M6 7h12v4a6 6 0 0 1-12 0zM12 17v4"/>',
5  chart: '<path d="M4 19V9M10 19V5M16 19v-7M22 19H2"/>',
6  form: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/>',
7  spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5L18 18M6 18l2.5-2.5M15.5 8.5L18 6"/>',
8  shield: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/>',
9};
10
11const megaItems = [
12  { icon: "bolt", title: "Automations", text: "Trigger anything, from anywhere" },
13  { icon: "plug", title: "Integrations", text: "200+ apps, connected in a click" },
14  { icon: "chart", title: "Analytics", text: "See what's working in real time" },
15  { icon: "form", title: "Forms", text: "Collect data that flows straight in" },
16  { icon: "spark", title: "AI Assistant", text: "Write, summarise and build faster" },
17  { icon: "shield", title: "Security", text: "SOC 2, SSO and audit logs" },
18];
19
20// Command palette entries: group, emoji, label, hint
21const commands = [
22  { group: "Pages", icon: "🏠", label: "Dashboard", hint: "Go to" },
23  { group: "Pages", icon: "💳", label: "Pricing", hint: "Go to" },
24  { group: "Pages", icon: "📚", label: "Documentation", hint: "Go to" },
25  { group: "Pages", icon: "📝", label: "Changelog", hint: "Go to" },
26  { group: "Pages", icon: "✍️", label: "Blog", hint: "Go to" },
27  { group: "Actions", icon: "⚡", label: "Create new workflow", hint: "Ctrl N" },
28  { group: "Actions", icon: "👥", label: "Invite a teammate", hint: "Action" },
29  { group: "Actions", icon: "🔗", label: "Copy link to this page", hint: "Action" },
30  { group: "Help", icon: "💬", label: "Contact support", hint: "Help" },
31  { group: "Help", icon: "⌨️", label: "Keyboard shortcuts", hint: "Help" },
32];
33
34const header = document.getElementById("header");
35const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
36const store = {
37  get: (k) => { try { return localStorage.getItem(k); } catch { return null; } },
38  set: (k, v) => { try { localStorage.setItem(k, v); } catch { /* storage blocked */ } },
39};
40
41const toast = (() => {
42  const el = document.getElementById("toast");
43  let t;
44  return (msg) => {
45    el.textContent = msg;
46    el.classList.add("show");
47    clearTimeout(t);
48    t = setTimeout(() => el.classList.remove("show"), 2200);
49  };
50})();
51
52// ---------- Mega menu content ----------
53document.getElementById("mega-grid").innerHTML = megaItems
54  .map(
55    (m) => `<a href="#" class="mega-item">
56      <span class="mi-icon"><svg viewBox="0 0 24 24" aria-hidden="true">${ICON[m.icon]}</svg></span>
57      <b>${m.title}</b><span>${m.text}</span>
58    </a>`,
59  )
60  .join("");
61
62// ---------- Announcement bar ----------
63const announce = document.getElementById("announce");
64if (store.get("announce-dismissed") === "3.0") announce.classList.add("is-hidden");
65announce.querySelector(".announce-close").addEventListener("click", () => {
66  announce.classList.add("is-hidden");
67  store.set("announce-dismissed", "3.0");
68});
69
70// ---------- Dropdown menus (hover intent + click + keyboard) ----------
71const menus = [...document.querySelectorAll(".has-menu")];
72let openTimer;
73let closeTimer;
74
75const setOpen = (li, open) => {
76  li.classList.toggle("is-open", open);
77  li.querySelector(".nav-link").setAttribute("aria-expanded", open);
78};
79const closeAll = (except) => menus.forEach((li) => li !== except && setOpen(li, false));
80
81menus.forEach((li) => {
82  const btn = li.querySelector(".nav-link");
83  const links = () => [...li.querySelectorAll(".menu a")];
84
85  li.addEventListener("pointerenter", (e) => {
86    if (e.pointerType !== "mouse") return;
87    clearTimeout(closeTimer);
88    // Small delay so menus don't flicker when the mouse just passes by
89    openTimer = setTimeout(() => {
90      closeAll(li);
91      setOpen(li, true);
92    }, menus.some((m) => m.classList.contains("is-open")) ? 0 : 90);
93  });
94  li.addEventListener("pointerleave", (e) => {
95    if (e.pointerType !== "mouse") return;
96    clearTimeout(openTimer);
97    closeTimer = setTimeout(() => setOpen(li, false), 180);
98  });
99
100  btn.addEventListener("click", () => {
101    const open = !li.classList.contains("is-open");
102    closeAll(li);
103    setOpen(li, open);
104  });
105
106  btn.addEventListener("keydown", (e) => {
107    if (e.key === "ArrowDown") {
108      e.preventDefault();
109      closeAll(li);
110      setOpen(li, true);
111      links()[0]?.focus();
112    }
113  });
114
115  li.querySelector(".menu").addEventListener("keydown", (e) => {
116    const items = links();
117    const i = items.indexOf(document.activeElement);
118    if (e.key === "ArrowDown") { e.preventDefault(); items[(i + 1) % items.length].focus(); }
119    if (e.key === "ArrowUp") { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); }
120    if (e.key === "Escape") { setOpen(li, false); btn.focus(); }
121  });
122
123  // Close when focus leaves the whole item (tabbing away)
124  li.addEventListener("focusout", (e) => {
125    if (!li.contains(e.relatedTarget)) setOpen(li, false);
126  });
127});
128
129document.addEventListener("click", (e) => {
130  if (!e.target.closest(".has-menu")) closeAll();
131});
132
133// Glide highlight behind the hovered nav link
134const nav = document.querySelector(".nav");
135const hover = document.querySelector(".nav-hover");
136nav.querySelectorAll(".nav-link").forEach((link) => {
137  link.addEventListener("pointerenter", () => {
138    const navBox = nav.getBoundingClientRect();
139    const box = link.getBoundingClientRect();
140    hover.style.setProperty("--x", `${box.left - navBox.left}px`);
141    hover.style.setProperty("--w", `${box.width}px`);
142    hover.style.height = `${box.height}px`;
143    hover.style.top = `${box.top - navBox.top}px`;
144    hover.classList.add("is-on");
145  });
146});
147nav.addEventListener("pointerleave", () => hover.classList.remove("is-on"));
148
149// ---------- Scroll behaviour: solid after scrolling, hide on down, show on up ----------
150let lastY = window.scrollY;
151let ticking = false;
152const onScroll = () => {
153  ticking = false;
154  const y = window.scrollY;
155  const anyOpen = menus.some((m) => m.classList.contains("is-open")) || drawer.classList.contains("is-open");
156  header.classList.toggle("is-scrolled", y > 10);
157  if (!anyOpen && Math.abs(y - lastY) > 6) {
158    header.classList.toggle("is-hidden", y > lastY && y > 160);
159    lastY = y;
160  }
161};
162window.addEventListener("scroll", () => {
163  if (!ticking) {
164    ticking = true;
165    requestAnimationFrame(onScroll);
166  }
167}, { passive: true });
168
169// ---------- Mobile drawer ----------
170const burger = document.getElementById("burger");
171const drawer = document.getElementById("drawer");
172
173// Build accordion sections from the desktop menus — one source of truth
174document.getElementById("drawer-acc").innerHTML = menus
175  .map((li, i) => {
176    const label = li.querySelector(".nav-link").textContent.trim();
177    const items = [...li.querySelectorAll(".menu a")]
178      .map((a) => `<a href="#">${(a.querySelector("b, strong") ?? a).textContent.trim()}</a>`)
179      .join("");
180    return `<button type="button" class="acc-btn" aria-expanded="false" aria-controls="acc-${i}">${label}
181      <svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
182      <div class="acc-panel" id="acc-${i}"><div>${items}</div></div>`;
183  })
184  .join("");
185
186document.querySelectorAll(".acc-btn").forEach((btn) => {
187  btn.addEventListener("click", () => {
188    const open = btn.getAttribute("aria-expanded") !== "true";
189    btn.setAttribute("aria-expanded", open);
190    btn.querySelector(".chev").style.transform = open ? "rotate(180deg)" : "";
191  });
192});
193
194const setDrawer = (open) => {
195  // The drawer starts right under the header, wherever it is right now
196  drawer.style.top = `${Math.max(header.getBoundingClientRect().bottom, 0)}px`;
197  drawer.classList.toggle("is-open", open);
198  drawer.setAttribute("aria-hidden", !open);
199  burger.setAttribute("aria-expanded", open);
200  burger.setAttribute("aria-label", open ? "Close menu" : "Open menu");
201  document.body.classList.toggle("no-scroll", open);
202};
203burger.addEventListener("click", () => setDrawer(!drawer.classList.contains("is-open")));
204window.addEventListener("resize", () => window.innerWidth > 900 && setDrawer(false));
205
206// ---------- Command palette ----------
207const palette = document.getElementById("palette");
208const input = document.getElementById("palette-input");
209const list = document.getElementById("palette-list");
210const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
211document.getElementById("kbd-hint").textContent = isMac ? "⌘ K" : "Ctrl K";
212
213let results = [];
214let active = 0;
215
216// Fuzzy match: every typed letter must appear in order. Returns matched indexes.
217const fuzzy = (text, query) => {
218  const hits = [];
219  let from = 0;
220  for (const ch of query.toLowerCase()) {
221    const i = text.toLowerCase().indexOf(ch, from);
222    if (i === -1) return null;
223    hits.push(i);
224    from = i + 1;
225  }
226  return hits;
227};
228
229const highlight = (text, hits) =>
230  [...text].map((ch, i) => (hits.includes(i) ? `<mark>${ch}</mark>` : ch)).join("");
231
232const recent = () => JSON.parse(store.get("palette-recent") || "[]");
233
234const renderPalette = () => {
235  const q = input.value.trim();
236  if (q) {
237    results = commands
238      .map((c) => ({ ...c, hits: fuzzy(c.label, q) }))
239      .filter((c) => c.hits)
240      // Tighter matches (letters closer together) first
241      .sort((a, b) => a.hits.at(-1) - a.hits[0] - (b.hits.at(-1) - b.hits[0]));
242  } else {
243    const rec = recent()
244      .map((label) => commands.find((c) => c.label === label))
245      .filter(Boolean)
246      .map((c) => ({ ...c, group: "Recent", hits: [] }));
247    results = [...rec, ...commands.map((c) => ({ ...c, hits: [] }))];
248  }
249  active = Math.min(active, Math.max(results.length - 1, 0));
250
251  if (!results.length) {
252    list.innerHTML = `<li class="palette-empty">No results for “${q.replace(/</g, "&lt;")}”</li>`;
253    return;
254  }
255  let group = "";
256  list.innerHTML = results
257    .map((c, i) => {
258      const heading = c.group !== group ? `<li class="palette-group" role="presentation">${(group = c.group)}</li>` : "";
259      return `${heading}<li class="palette-item" role="option" id="opt-${i}" data-i="${i}" aria-selected="${i === active}">
260        <span class="pi-icon" aria-hidden="true">${c.icon}</span><span>${highlight(c.label, c.hits)}</span><small>${c.hint}</small></li>`;
261    })
262    .join("");
263  input.setAttribute("aria-activedescendant", `opt-${active}`);
264};
265
266const setActive = (i) => {
267  active = (i + results.length) % results.length;
268  list.querySelectorAll(".palette-item").forEach((el) => el.setAttribute("aria-selected", Number(el.dataset.i) === active));
269  list.querySelector(`[data-i="${active}"]`)?.scrollIntoView({ block: "nearest" });
270  input.setAttribute("aria-activedescendant", `opt-${active}`);
271};
272
273const runCommand = (c) => {
274  if (!c) return;
275  store.set("palette-recent", JSON.stringify([c.label, ...recent().filter((l) => l !== c.label)].slice(0, 3)));
276  palette.close();
277  toast(`${c.icon}  ${c.label}`);
278};
279
280const openPalette = () => {
281  if (palette.open) return;
282  setDrawer(false);
283  closeAll();
284  input.value = "";
285  active = 0;
286  renderPalette();
287  palette.showModal();
288  input.focus();
289};
290
291document.getElementById("open-palette").addEventListener("click", openPalette);
292document.querySelectorAll("[data-open-palette]").forEach((b) => b.addEventListener("click", openPalette));
293input.addEventListener("input", () => {
294  active = 0;
295  renderPalette();
296});
297input.addEventListener("keydown", (e) => {
298  if (e.key === "ArrowDown") { e.preventDefault(); setActive(active + 1); }
299  if (e.key === "ArrowUp") { e.preventDefault(); setActive(active - 1); }
300  if (e.key === "Enter") { e.preventDefault(); runCommand(results[active]); }
301});
302list.addEventListener("pointermove", (e) => {
303  const item = e.target.closest(".palette-item");
304  if (item && Number(item.dataset.i) !== active) setActive(Number(item.dataset.i));
305});
306list.addEventListener("click", (e) => {
307  const item = e.target.closest(".palette-item");
308  if (item) runCommand(results[Number(item.dataset.i)]);
309});
310palette.addEventListener("click", (e) => e.target === palette && palette.close());
311
312// Global shortcuts: Ctrl/⌘ K for the palette, Esc closes menus and the drawer
313document.addEventListener("keydown", (e) => {
314  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
315    e.preventDefault();
316    palette.open ? palette.close() : openPalette();
317  }
318  if (e.key === "Escape") {
319    closeAll();
320    if (drawer.classList.contains("is-open")) {
321      setDrawer(false);
322      burger.focus();
323    }
324  }
325});
326
327if (reduceMotion) header.style.transition = "none";

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

Features

  • Mega menu with icon grid and a featured card, plus simple list dropdowns

  • Hover intent (no flicker), click and full keyboard support (↓, ↑, Esc)

  • Highlight pill that glides between nav links

  • ⌘K / Ctrl K command palette with fuzzy search, highlighted matches and recent items

  • Turns solid + blurred after scrolling, hides on scroll down, returns on scroll up

  • Dismissible announcement bar (remembered)

  • Mobile drawer with accordion sections built from the desktop menus

Built With

🌐

HTML5

🎨

CSS3

⚡

JavaScript

Compatibility

Browsers
ChromeEdgeFirefoxSafariOperaBraveArcVivaldi
Devices
DesktopTabletMobile
Frameworks
Vanilla JSHTML/CSS

❓ FAQs

Edit the commands array at the top of script.js. Each item has a group, an emoji icon, a label and a hint. Put your navigation or action inside runCommand().

Yes. In onScroll() remove the line that toggles the is-hidden class — the header will stay sticky and still turn solid after scrolling.

No. The mobile drawer's accordion is built automatically from the desktop dropdowns, so you only edit one menu.

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.

SaaS Header with Mega Menu & Command Palette | MetaSkillGroup