Floating Island Navbar with Scroll Progress & Mobile Tab Bar
A floating glass 'island' navbar that shrinks as you scroll, with a scroll-progress ring around the logo, a sliding pill that follows the active section (scrollspy) and previews on hover, a notifications panel with unread badge and mark-all-read, and an account menu with online / away / busy status. On phones the links move into an app-style bottom tab bar with a sliding bubble.
- Published 15 Oct 2026
- 9 views
Overview
Floating Island Navbar with Scroll Progress & Mobile Tab Bar
Navigation that floats above it all — progress ring, scrollspy pill, notifications and an app-like tab bar on phones.
What's Included
- Floating glass island that compacts after you start scrolling
- Scroll-progress ring around the logo
- Scrollspy: the pill slides to the section you're reading (and previews on hover)
- Notifications panel with unread badge, mark-all-read and a live 'new' bell ring
- Account menu with online / away / busy status
- Panels unfold from the island with a clip-path animation
- Mobile bottom tab bar with sliding active bubble
Dependencies
- Google Fonts — Urbanist https://fonts.googleapis.com/css2?family=Urbanist:wght@400;500;600;700;800;900&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>Floating Island Navbar | MetaSkillGroup</title> 7 <link rel="stylesheet" href="style.css" /> 8 </head> 9 <body> 10 <!-- created by metaskillgroup-team --> 11 <nav class="island" id="island" aria-label="Main"> 12 <a href="#home" class="brand" aria-label="Orbit — back to top"> 13 <svg class="ring" viewBox="0 0 44 44" aria-hidden="true"> 14 <circle class="ring-track" cx="22" cy="22" r="19" /> 15 <circle class="ring-fill" id="ring-fill" cx="22" cy="22" r="19" /> 16 </svg> 17 <span class="brand-dot" aria-hidden="true"></span> 18 </a> 19 20 <ul class="links" id="links"> 21 <li><a href="#home" data-section="home">Home</a></li> 22 <li><a href="#features" data-section="features">Features</a></li> 23 <li><a href="#work" data-section="work">Work</a></li> 24 <li><a href="#pricing" data-section="pricing">Pricing</a></li> 25 <li><a href="#contact" data-section="contact">Contact</a></li> 26 <li class="pill" aria-hidden="true"></li> 27 </ul> 28 29 <div class="tools"> 30 <div class="pop"> 31 <button type="button" class="icon-btn" id="bell-btn" aria-label="Notifications" aria-expanded="false" aria-controls="bell-panel"> 32 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10.3 21a1.9 1.9 0 0 0 3.4 0" /></svg> 33 <span class="badge" id="badge">3</span> 34 </button> 35 <div class="panel" id="bell-panel" role="dialog" aria-label="Notifications"> 36 <div class="panel-head"> 37 <strong>Notifications</strong> 38 <button type="button" class="link-btn" id="mark-all">Mark all as read</button> 39 </div> 40 <ul class="notes" id="notes"></ul> 41 </div> 42 </div> 43 44 <div class="pop"> 45 <button type="button" class="avatar-btn" id="avatar-btn" aria-label="Account menu" aria-expanded="false" aria-controls="avatar-panel"> 46 <span class="avatar">AK</span> 47 <span class="status-dot" id="status-dot" data-status="online"></span> 48 </button> 49 <div class="panel" id="avatar-panel" role="menu" aria-label="Account"> 50 <div class="profile"> 51 <span class="avatar big">AK</span> 52 <div> 53 <strong>Ankul Kumar</strong> 54 <small>ankul@orbit.dev</small> 55 </div> 56 </div> 57 <div class="status-row" role="group" aria-label="Status"> 58 <button type="button" data-status="online" aria-pressed="true"><i></i>Online</button> 59 <button type="button" data-status="away" aria-pressed="false"><i></i>Away</button> 60 <button type="button" data-status="dnd" aria-pressed="false"><i></i>Busy</button> 61 </div> 62 <a href="#" role="menuitem">👤 Profile</a> 63 <a href="#" role="menuitem">⚙️ Settings</a> 64 <a href="#" role="menuitem">💳 Billing <span class="tag">Pro</span></a> 65 <a href="#" role="menuitem">⌨️ Shortcuts <kbd>?</kbd></a> 66 <hr /> 67 <a href="#" role="menuitem" class="danger">↩ Sign out</a> 68 </div> 69 </div> 70 </div> 71 </nav> 72 73 <!-- Mobile bottom tab bar --> 74 <nav class="tabbar" aria-label="Sections"> 75 <span class="tab-bubble" aria-hidden="true"></span> 76 <a href="#home" data-section="home"><svg viewBox="0 0 24 24"><path d="M3 11l9-8 9 8v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" /></svg><span>Home</span></a> 77 <a href="#features" data-section="features"><svg viewBox="0 0 24 24"><path d="M13 3L4 14h7l-1 7 9-11h-7z" /></svg><span>Features</span></a> 78 <a href="#work" data-section="work"><svg viewBox="0 0 24 24"><rect x="3" y="7" width="18" height="13" rx="2" /><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg><span>Work</span></a> 79 <a href="#pricing" data-section="pricing"><svg viewBox="0 0 24 24"><path d="M20 12l-8 8-9-9V3h8z" /><circle cx="7.5" cy="7.5" r="1.5" /></svg><span>Pricing</span></a> 80 <a href="#contact" data-section="contact"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg><span>Contact</span></a> 81 </nav> 82 83 <!-- Demo sections --> 84 <main> 85 <section id="home" class="sec" style="--c1: #7c5cff; --c2: #22d3ee"> 86 <span class="eyebrow">Floating island navbar</span> 87 <h1>Navigation that<br />floats above it all.</h1> 88 <p>Scroll — the island shrinks, the ring around the logo fills and the active section follows you. Try the bell and the avatar.</p> 89 </section> 90 <section id="features" class="sec" style="--c1: #f43f5e; --c2: #f59e0b"> 91 <h2>Features</h2> 92 <div class="cards"><div></div><div></div><div></div></div> 93 </section> 94 <section id="work" class="sec" style="--c1: #10b981; --c2: #3b82f6"> 95 <h2>Work</h2> 96 <div class="cards"><div></div><div></div><div></div></div> 97 </section> 98 <section id="pricing" class="sec" style="--c1: #a855f7; --c2: #ec4899"> 99 <h2>Pricing</h2> 100 <div class="cards"><div></div><div></div><div></div></div> 101 </section> 102 <section id="contact" class="sec" style="--c1: #06b6d4; --c2: #84cc16"> 103 <h2>Contact</h2> 104 <div class="cards"><div></div><div></div><div></div></div> 105 </section> 106 </main> 107 108 <script src="script.js"></script> 109 </body> 110</html>
CSS
css1/* Importing Google Fonts - Urbanist */ 2@import url("https://fonts.googleapis.com/css2?family=Urbanist: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: "Urbanist", sans-serif; 9} 10 11:root { 12 --bg: #07080c; 13 --glass: rgba(22, 24, 33, 0.72); 14 --glass-solid: rgba(16, 17, 24, 0.92); 15 --line: rgba(255, 255, 255, 0.1); 16 --text: #f3f4f8; 17 --muted: #9599ab; 18 --accent: #8b7cff; 19 --ease: cubic-bezier(0.2, 0.8, 0.2, 1); 20} 21 22html { 23 scroll-behavior: smooth; 24} 25 26body { 27 background: var(--bg); 28 color: var(--text); 29} 30 31a { 32 color: inherit; 33 text-decoration: none; 34} 35 36/* ========================================================= 37 The island 38 ========================================================= */ 39.island { 40 position: fixed; 41 top: 18px; 42 left: 50%; 43 z-index: 50; 44 display: flex; 45 align-items: center; 46 gap: 10px; 47 padding: 7px; 48 border: 1px solid var(--line); 49 border-radius: 999px; 50 background: var(--glass); 51 backdrop-filter: blur(18px) saturate(1.5); 52 box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.06); 53 transform: translateX(-50%); 54 transition: padding 0.4s var(--ease), gap 0.4s var(--ease), background 0.4s, top 0.4s var(--ease); 55} 56 57/* Compact once you start scrolling */ 58.island.is-compact { 59 top: 12px; 60 gap: 6px; 61 padding: 5px; 62 background: var(--glass-solid); 63} 64 65/* Logo with a scroll-progress ring around it */ 66.brand { 67 position: relative; 68 display: grid; 69 place-items: center; 70 width: 44px; 71 height: 44px; 72 flex-shrink: 0; 73} 74 75.ring { 76 position: absolute; 77 inset: 0; 78 transform: rotate(-90deg); 79} 80 81.ring circle { 82 fill: none; 83 stroke-width: 3; 84} 85 86.ring-track { 87 stroke: rgba(255, 255, 255, 0.08); 88} 89 90.ring-fill { 91 stroke: var(--accent); 92 stroke-linecap: round; 93 stroke-dasharray: 119.4; 94 stroke-dashoffset: calc(119.4 * (1 - var(--progress, 0))); 95} 96 97.brand-dot { 98 width: 22px; 99 height: 22px; 100 border-radius: 50%; 101 background: conic-gradient(from 200deg, #8b7cff, #22d3ee, #f472b6, #8b7cff); 102 animation: spin 8s linear infinite; 103} 104 105@keyframes spin { 106 to { 107 transform: rotate(360deg); 108 } 109} 110 111/* Links + sliding pill */ 112.links { 113 position: relative; 114 display: flex; 115 list-style: none; 116} 117 118.links a { 119 position: relative; 120 z-index: 1; 121 display: block; 122 padding: 10px 16px; 123 border-radius: 999px; 124 color: var(--muted); 125 font-size: 0.95rem; 126 font-weight: 600; 127 transition: color 0.3s, padding 0.4s var(--ease); 128} 129 130.island.is-compact .links a { 131 padding: 8px 13px; 132} 133 134.links a:hover, 135.links a.is-active { 136 color: var(--text); 137} 138 139.links a:focus-visible, 140.icon-btn:focus-visible, 141.avatar-btn:focus-visible, 142.brand:focus-visible { 143 outline: 2px solid var(--accent); 144 outline-offset: 2px; 145 border-radius: 999px; 146} 147 148.pill { 149 position: absolute; 150 top: 0; 151 left: 0; 152 height: 100%; 153 width: var(--w, 0); 154 border-radius: 999px; 155 background: rgba(255, 255, 255, 0.1); 156 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); 157 transform: translateX(var(--x, 0)); 158 transition: transform 0.45s var(--ease), width 0.45s var(--ease); 159} 160 161/* Tools on the right */ 162.tools { 163 display: flex; 164 align-items: center; 165 gap: 6px; 166} 167 168.pop { 169 position: relative; 170} 171 172.icon-btn, 173.avatar-btn { 174 position: relative; 175 display: grid; 176 place-items: center; 177 width: 44px; 178 height: 44px; 179 border: 0; 180 border-radius: 50%; 181 background: rgba(255, 255, 255, 0.06); 182 color: var(--text); 183 cursor: pointer; 184 transition: background 0.2s; 185} 186 187.icon-btn:hover, 188.icon-btn[aria-expanded="true"] { 189 background: rgba(255, 255, 255, 0.14); 190} 191 192.icon-btn svg, 193.tabbar svg { 194 width: 20px; 195 height: 20px; 196 fill: none; 197 stroke: currentColor; 198 stroke-width: 2; 199 stroke-linecap: round; 200 stroke-linejoin: round; 201} 202 203.icon-btn[aria-expanded="true"] svg, 204.icon-btn.ring-it svg { 205 animation: ringBell 0.6s ease; 206} 207 208@keyframes ringBell { 209 20% { transform: rotate(14deg); } 210 40% { transform: rotate(-12deg); } 211 60% { transform: rotate(8deg); } 212 80% { transform: rotate(-4deg); } 213} 214 215.badge { 216 position: absolute; 217 top: 6px; 218 right: 6px; 219 display: grid; 220 place-items: center; 221 min-width: 17px; 222 height: 17px; 223 padding: 0 4px; 224 border: 2px solid #15161f; 225 border-radius: 999px; 226 background: #f43f5e; 227 font-size: 0.65rem; 228 font-weight: 800; 229 transition: transform 0.3s var(--ease); 230} 231 232.badge.is-empty { 233 transform: scale(0); 234} 235 236.avatar-btn { 237 background: none; 238} 239 240.avatar { 241 display: grid; 242 place-items: center; 243 width: 38px; 244 height: 38px; 245 border-radius: 50%; 246 background: linear-gradient(135deg, #f472b6, #8b7cff); 247 font-size: 0.8rem; 248 font-weight: 800; 249} 250 251.avatar.big { 252 width: 46px; 253 height: 46px; 254 font-size: 0.95rem; 255} 256 257.status-dot { 258 position: absolute; 259 right: 3px; 260 bottom: 3px; 261 width: 12px; 262 height: 12px; 263 border: 2px solid #15161f; 264 border-radius: 50%; 265 background: #22c55e; 266} 267 268.status-dot[data-status="away"] { background: #f59e0b; } 269.status-dot[data-status="dnd"] { background: #f43f5e; } 270 271/* ---------- Dropdown panels (unfold from the island) ---------- */ 272.panel { 273 position: absolute; 274 top: calc(100% + 16px); 275 right: -7px; 276 width: 340px; 277 padding: 10px; 278 border: 1px solid var(--line); 279 border-radius: 24px; 280 background: rgba(17, 18, 26, 0.97); 281 box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.85); 282 opacity: 0; 283 visibility: hidden; 284 transform: translateY(-12px) scale(0.92); 285 transform-origin: top right; 286 clip-path: inset(0 0 100% 0 round 24px); 287 transition: opacity 0.25s, transform 0.4s var(--ease), clip-path 0.45s var(--ease), visibility 0.4s; 288} 289 290.pop.is-open .panel { 291 opacity: 1; 292 visibility: visible; 293 transform: none; 294 clip-path: inset(0 0 0 0 round 24px); 295} 296 297.panel-head { 298 display: flex; 299 justify-content: space-between; 300 align-items: center; 301 padding: 8px 10px 12px; 302} 303 304.link-btn { 305 border: 0; 306 background: none; 307 color: var(--accent); 308 font-size: 0.85rem; 309 font-weight: 700; 310 cursor: pointer; 311} 312 313.notes { 314 display: grid; 315 gap: 2px; 316 list-style: none; 317} 318 319.note { 320 position: relative; 321 display: grid; 322 grid-template-columns: 38px 1fr; 323 gap: 12px; 324 padding: 10px 28px 10px 10px; 325 border-radius: 16px; 326 cursor: pointer; 327 transition: background 0.2s; 328} 329 330.note:hover { 331 background: rgba(255, 255, 255, 0.06); 332} 333 334.note .n-av { 335 display: grid; 336 place-items: center; 337 width: 38px; 338 height: 38px; 339 border-radius: 12px; 340 font-size: 1.1rem; 341 background: rgba(255, 255, 255, 0.08); 342} 343 344.note p { 345 color: var(--muted); 346 font-size: 0.88rem; 347 line-height: 1.4; 348} 349 350.note p b { 351 color: var(--text); 352} 353 354.note small { 355 color: #6b6f82; 356 font-size: 0.75rem; 357} 358 359.note.unread::after { 360 content: ""; 361 position: absolute; 362 top: 50%; 363 right: 12px; 364 width: 8px; 365 height: 8px; 366 border-radius: 50%; 367 background: var(--accent); 368 transform: translateY(-50%); 369} 370 371.profile { 372 display: flex; 373 align-items: center; 374 gap: 12px; 375 padding: 8px 10px 14px; 376} 377 378.profile small { 379 display: block; 380 color: var(--muted); 381} 382 383.status-row { 384 display: grid; 385 grid-template-columns: repeat(3, 1fr); 386 gap: 4px; 387 margin: 0 4px 8px; 388 padding: 4px; 389 border-radius: 14px; 390 background: rgba(255, 255, 255, 0.05); 391} 392 393.status-row button { 394 display: flex; 395 align-items: center; 396 justify-content: center; 397 gap: 6px; 398 padding: 8px; 399 border: 0; 400 border-radius: 10px; 401 background: none; 402 color: var(--muted); 403 font-size: 0.82rem; 404 font-weight: 700; 405 cursor: pointer; 406} 407 408.status-row i { 409 width: 8px; 410 height: 8px; 411 border-radius: 50%; 412 background: #22c55e; 413} 414 415.status-row [data-status="away"] i { background: #f59e0b; } 416.status-row [data-status="dnd"] i { background: #f43f5e; } 417 418.status-row [aria-pressed="true"] { 419 background: rgba(255, 255, 255, 0.1); 420 color: var(--text); 421} 422 423#avatar-panel { 424 width: 280px; 425} 426 427#avatar-panel > a { 428 display: flex; 429 align-items: center; 430 gap: 10px; 431 padding: 11px 12px; 432 border-radius: 12px; 433 font-size: 0.93rem; 434 font-weight: 600; 435 transition: background 0.2s; 436} 437 438#avatar-panel > a:hover, 439#avatar-panel > a:focus-visible { 440 background: rgba(255, 255, 255, 0.07); 441 outline: none; 442} 443 444#avatar-panel .tag { 445 margin-left: auto; 446 padding: 2px 8px; 447 border-radius: 999px; 448 background: rgba(139, 124, 255, 0.2); 449 color: #c4bcff; 450 font-size: 0.72rem; 451} 452 453#avatar-panel kbd { 454 margin-left: auto; 455 padding: 1px 7px; 456 border: 1px solid var(--line); 457 border-radius: 6px; 458 color: var(--muted); 459 font-family: inherit; 460 font-size: 0.75rem; 461} 462 463#avatar-panel hr { 464 margin: 6px 8px; 465 border: 0; 466 border-top: 1px solid var(--line); 467} 468 469#avatar-panel .danger { 470 color: #fb7185; 471} 472 473/* ========================================================= 474 Mobile tab bar 475 ========================================================= */ 476.tabbar { 477 position: fixed; 478 left: 50%; 479 bottom: 14px; 480 z-index: 50; 481 display: none; 482 grid-template-columns: repeat(5, 1fr); 483 width: min(440px, calc(100% - 24px)); 484 padding: 6px; 485 border: 1px solid var(--line); 486 border-radius: 24px; 487 background: var(--glass-solid); 488 backdrop-filter: blur(18px); 489 box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.9); 490 transform: translateX(-50%); 491} 492 493.tabbar a { 494 position: relative; 495 z-index: 1; 496 display: grid; 497 justify-items: center; 498 gap: 3px; 499 padding: 8px 0; 500 color: var(--muted); 501 font-size: 0.7rem; 502 font-weight: 700; 503 transition: color 0.3s; 504} 505 506.tabbar a.is-active { 507 color: var(--text); 508} 509 510.tab-bubble { 511 position: absolute; 512 top: 6px; 513 bottom: 6px; 514 left: 6px; 515 width: calc((100% - 12px) / 5); 516 border-radius: 18px; 517 background: rgba(139, 124, 255, 0.22); 518 transform: translateX(calc(var(--i, 0) * 100%)); 519 transition: transform 0.45s var(--ease); 520} 521 522/* ========================================================= 523 Demo sections 524 ========================================================= */ 525.sec { 526 min-height: 92vh; 527 padding: 150px 24px 80px; 528 max-width: 1100px; 529 margin: 0 auto; 530 scroll-margin-top: 0; 531} 532 533.eyebrow { 534 color: var(--accent); 535 font-size: 0.85rem; 536 font-weight: 800; 537 letter-spacing: 0.16em; 538 text-transform: uppercase; 539} 540 541.sec h1, 542.sec h2 { 543 margin: 14px 0 18px; 544 font-size: clamp(2.6rem, 8vw, 5.6rem); 545 font-weight: 900; 546 line-height: 0.95; 547 letter-spacing: -0.04em; 548 background: linear-gradient(100deg, var(--c1), var(--c2)); 549 -webkit-background-clip: text; 550 background-clip: text; 551 color: transparent; 552} 553 554.sec p { 555 max-width: 560px; 556 color: var(--muted); 557 font-size: 1.15rem; 558 line-height: 1.6; 559} 560 561.cards { 562 display: grid; 563 grid-template-columns: repeat(3, 1fr); 564 gap: 18px; 565 margin-top: 30px; 566} 567 568.cards div { 569 height: 280px; 570 border: 1px solid var(--line); 571 border-radius: 26px; 572 background: 573 radial-gradient(circle at 20% 0%, color-mix(in srgb, var(--c1) 35%, transparent), transparent 60%), 574 radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--c2) 25%, transparent), transparent 60%), 575 #101219; 576} 577 578/* ---------- Responsive ---------- */ 579@media (max-width: 760px) { 580 .links { 581 display: none; 582 } 583 584 .island { 585 left: 12px; 586 right: 12px; 587 justify-content: space-between; 588 transform: none; 589 } 590 591 .tabbar { 592 display: grid; 593 } 594 595 /* The island (fixed + backdrop-filter) becomes the panel's box */ 596 .pop { 597 position: static; 598 } 599 600 .panel { 601 top: calc(100% + 12px); 602 left: 0; 603 right: 0; 604 width: auto !important; 605 transform-origin: top center; 606 } 607 608 .cards { 609 grid-template-columns: 1fr; 610 } 611 612 .sec { 613 padding-top: 120px; 614 } 615} 616 617@media (prefers-reduced-motion: reduce) { 618 html { 619 scroll-behavior: auto; 620 } 621 622 *, 623 *::before, 624 *::after { 625 animation: none !important; 626 transition: none !important; 627 } 628}
JavaScript
javascript1// Edit these notifications freely 2const notifications = [ 3 { icon: "🚀", text: "<b>Riya</b> deployed <b>orbit-web</b> to production", time: "2 min ago", unread: true }, 4 { icon: "💬", text: "<b>Kabir</b> replied to your comment on <b>Landing v2</b>", time: "18 min ago", unread: true }, 5 { icon: "⭐", text: "Your project got <b>12 new stars</b> today", time: "1 h ago", unread: true }, 6 { icon: "🧾", text: "Invoice <b>#1043</b> was paid", time: "Yesterday", unread: false }, 7]; 8 9const island = document.getElementById("island"); 10const links = [...document.querySelectorAll("#links a")]; 11const pill = document.querySelector("#links .pill"); 12const ringFill = document.getElementById("ring-fill"); 13const tabLinks = [...document.querySelectorAll(".tabbar a")]; 14const tabBubble = document.querySelector(".tab-bubble"); 15const sections = links.map((a) => document.getElementById(a.dataset.section)); 16let activeId = "home"; 17 18// ---------- Sliding pill ---------- 19const movePill = (link) => { 20 pill.style.setProperty("--x", `${link.offsetLeft}px`); 21 pill.style.setProperty("--w", `${link.offsetWidth}px`); 22}; 23const activeLink = () => links.find((a) => a.dataset.section === activeId); 24 25links.forEach((a) => { 26 a.addEventListener("pointerenter", () => movePill(a)); // preview on hover 27}); 28document.getElementById("links").addEventListener("pointerleave", () => movePill(activeLink())); 29 30const setActive = (id) => { 31 if (id === activeId && pill.style.getPropertyValue("--w")) return; 32 activeId = id; 33 links.forEach((a) => { 34 const on = a.dataset.section === id; 35 a.classList.toggle("is-active", on); 36 on ? a.setAttribute("aria-current", "true") : a.removeAttribute("aria-current"); 37 }); 38 tabLinks.forEach((a, i) => { 39 const on = a.dataset.section === id; 40 a.classList.toggle("is-active", on); 41 if (on) tabBubble.style.setProperty("--i", i); 42 }); 43 movePill(activeLink()); 44}; 45 46// ---------- Scroll: compact island, progress ring, scrollspy ---------- 47let ticking = false; 48const onScroll = () => { 49 ticking = false; 50 const max = document.documentElement.scrollHeight - innerHeight; 51 const progress = max > 0 ? scrollY / max : 0; 52 ringFill.style.setProperty("--progress", progress.toFixed(4)); 53 island.classList.toggle("is-compact", scrollY > 40); 54 55 // The section whose top has passed 40% of the screen is the active one 56 const line = innerHeight * 0.4; 57 let current = sections[0].id; 58 for (const s of sections) if (s.getBoundingClientRect().top <= line) current = s.id; 59 if (progress > 0.995) current = sections.at(-1).id; 60 setActive(current); 61}; 62window.addEventListener("scroll", () => { 63 if (!ticking) { 64 ticking = true; 65 requestAnimationFrame(onScroll); 66 } 67}, { passive: true }); 68// Re-measure after the compact transition / font load / resize 69island.addEventListener("transitionend", () => movePill(activeLink())); 70window.addEventListener("resize", onScroll); 71document.fonts?.ready.then(() => movePill(activeLink())); 72 73// ---------- Dropdowns ---------- 74const pops = [...document.querySelectorAll(".pop")]; 75const closePops = (except) => 76 pops.forEach((p) => { 77 if (p === except) return; 78 p.classList.remove("is-open"); 79 p.querySelector("button").setAttribute("aria-expanded", "false"); 80 }); 81 82pops.forEach((pop) => { 83 const btn = pop.querySelector("button"); 84 btn.addEventListener("click", (e) => { 85 e.stopPropagation(); 86 const open = !pop.classList.contains("is-open"); 87 closePops(pop); 88 pop.classList.toggle("is-open", open); 89 btn.setAttribute("aria-expanded", open); 90 }); 91 pop.querySelector(".panel").addEventListener("click", (e) => e.stopPropagation()); 92}); 93document.addEventListener("click", () => closePops()); 94document.addEventListener("keydown", (e) => { 95 if (e.key !== "Escape") return; 96 const open = pops.find((p) => p.classList.contains("is-open")); 97 if (open) { 98 closePops(); 99 open.querySelector("button").focus(); 100 } 101}); 102 103// ---------- Notifications ---------- 104const notesEl = document.getElementById("notes"); 105const badge = document.getElementById("badge"); 106const bellBtn = document.getElementById("bell-btn"); 107 108const renderNotes = () => { 109 notesEl.innerHTML = notifications 110 .map((n, i) => `<li class="note ${n.unread ? "unread" : ""}" data-i="${i}"> 111 <span class="n-av" aria-hidden="true">${n.icon}</span> 112 <div><p>${n.text}</p><small>${n.time}</small></div></li>`) 113 .join(""); 114 const unread = notifications.filter((n) => n.unread).length; 115 badge.textContent = unread; 116 badge.classList.toggle("is-empty", unread === 0); 117 bellBtn.setAttribute("aria-label", unread ? `Notifications, ${unread} unread` : "Notifications"); 118}; 119 120notesEl.addEventListener("click", (e) => { 121 const li = e.target.closest(".note"); 122 if (!li) return; 123 notifications[li.dataset.i].unread = false; 124 renderNotes(); 125}); 126document.getElementById("mark-all").addEventListener("click", () => { 127 notifications.forEach((n) => (n.unread = false)); 128 renderNotes(); 129}); 130 131// A new notification "arrives" a few seconds after load 132setTimeout(() => { 133 notifications.unshift({ icon: "🎉", text: "<b>Welcome!</b> Your workspace is ready.", time: "Just now", unread: true }); 134 renderNotes(); 135 bellBtn.classList.remove("ring-it"); 136 void bellBtn.offsetWidth; 137 bellBtn.classList.add("ring-it"); 138}, 4000); 139 140// ---------- Status (online / away / busy) ---------- 141const statusDot = document.getElementById("status-dot"); 142document.querySelectorAll(".status-row button").forEach((b) => { 143 b.addEventListener("click", () => { 144 document.querySelectorAll(".status-row button").forEach((x) => x.setAttribute("aria-pressed", x === b)); 145 statusDot.dataset.status = b.dataset.status; 146 }); 147}); 148 149renderNotes(); 150setActive("home"); 151onScroll();
Built by MetaSkillGroup — copy it, use it, make it yours.
Features
Floating glass island that compacts after you start scrolling
Scroll-progress ring around the logo
Scrollspy: the pill slides to the section you're reading (and previews on hover)
Notifications panel with unread badge, mark-all-read and a live 'new' bell ring
Account menu with online / away / busy status
Panels unfold from the island with a clip-path animation
Mobile bottom tab bar with sliding active bubble
Built With
HTML5
CSS3
JavaScript
Compatibility
❓ FAQs
On scroll, script.js checks which section's top has passed 40% of the screen and moves the pill to its link. Add data-section to a link and give the section the same id.
Edit the notifications array at the top of script.js (icon, text, time, unread). The badge count updates automatically.
Yes — delete the .tabbar nav, or keep the desktop links visible on phones by removing the .links { display: none } rule in the mobile media query.

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.