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
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
- Google Fonts — Instrument Sans + Instrument Serif https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@1&display=swap
Already linked inside the component's files — no extra setup needed.
Desktop Preview
HTML
html1<!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
css1/* 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
javascript1// ---------- 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, "<")}”</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
❓ 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.

Written by
MetaSkillGroup
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.