ZhuoQi Devzhuoqidev.comRSSllms.txt

← All entries~/design/pill-dock

NavigationOriginal

Floating Pill Nav at the Bottom

A pill-shaped nav centered at the page bottom: frosted glass, highlighted current section, bounce on press, twinkling stars.

html · cssMITAdded #navigation#pill#handmade#frosted glass

Live demoOpen in a new tab ↗

What makes it work

  • Position: position: fixed + left: 50% + translateX(-50%), always stuck at the bottom center, within thumb reach on phones.
  • Frosted glass: the background is color-mix(in srgb, var(--card) 82%, transparent) plus backdrop-filter: blur(16px), so it stays readable over content.
  • Bounce: scale: .93 on press, and on release it springs back with a spring curve written in linear(), with no JS animation library.
  • Numbering: a small monospace number (01–04) before each item gives the row of text some rhythm.

How to use it

  1. Copy the .dock and .stars CSS blocks into your stylesheet and swap the color variables for your own.
  2. Add aria-current="page" to the link for the current page (set on the server or in the router); both the highlight and screen readers rely on it.
  3. Leave padding-bottom at the bottom of the page so the last section isn’t covered by the nav.

Pitfalls

  • backdrop-filter needs the -webkit- prefix in Safari.
  • In older browsers that don’t support linear() spring curves, the transition falls back to linear, which doesn’t affect usability.
$For agents: index.md

Source MIT licensed. Take it.

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>底部悬浮药丸导航</title>
    <style>
      :root {
        --paper: #fbf8f1;
        --paper-2: #f3eee2;
        --card: #fffdf8;
        --ink: #1c2230;
        --ink-2: #4a5163;
        --ink-3: #636877;
        --line: #e6dfd0;
        --dot: #d8d0bf;
        --blue: #2f5bd3;
        --on-blue: #fff;
        --hi: #ffe066;
        --ease: cubic-bezier(0.16, 1, 0.3, 1);
        /* A spring as a CSS easing curve: overshoots a little, then settles. */
        --spring: linear(
          0,
          0.062,
          0.215,
          0.411,
          0.615,
          0.799,
          0.95,
          1.059,
          1.126,
          1.158,
          1.161,
          1.145,
          1.116,
          1.083,
          1.051,
          1.023,
          1.001,
          0.986,
          0.977,
          0.974,
          0.974,
          0.978,
          0.983,
          0.988,
          0.994,
          0.998,
          1.001,
          1.003,
          1.004,
          1.004,
          1
        );
        --f-sans: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
        --f-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
        color-scheme: light;
      }
      @media (prefers-color-scheme: dark) {
        :root {
          --paper: #14161b;
          --paper-2: #1c1f26;
          --card: #1e2129;
          --ink: #ece7dc;
          --ink-2: #bab6ac;
          --ink-3: #959aa8;
          --line: #31353f;
          --dot: #2c3039;
          --blue: #8eaaff;
          --on-blue: #14161b;
          --hi: #7d6416;
          color-scheme: dark;
        }
      }
      body {
        margin: 0;
        min-height: 100vh;
        background: var(--paper) radial-gradient(var(--dot) 1px, transparent 1.2px) 0 0 / 22px 22px;
        color: var(--ink);
        font: 16px/1.7 var(--f-sans);
      }
      main {
        max-width: 560px;
        margin: 0 auto;
        padding: 56px 16px 140px;
      }
      h1 {
        font-size: 28px;
        margin: 0 0 8px;
      }
      p {
        color: var(--ink-2);
      }

      /* ---------- the dock ---------- */
      .dock {
        position: fixed;
        left: 50%;
        bottom: 18px;
        z-index: 50;
        transform: translateX(-50%);
        display: flex;
        gap: 4px;
        padding: 6px;
        background: color-mix(in srgb, var(--card) 82%, transparent);
        border: 1px solid var(--line);
        -webkit-backdrop-filter: blur(16px);
        backdrop-filter: blur(16px);
        border-radius: 999px;
        box-shadow: 0 8px 32px rgba(28, 34, 48, 0.12);
      }
      .dock a {
        display: flex;
        align-items: baseline;
        gap: 6px;
        padding: 9px 16px;
        border-radius: 999px;
        font: 500 14px/1 var(--f-sans);
        text-decoration: none;
        color: var(--ink-2);
        white-space: nowrap;
        transition:
          scale 0.45s var(--spring),
          background 0.25s var(--ease),
          color 0.25s var(--ease);
      }
      .dock a .k {
        font: 400 10px/1 var(--f-mono);
        color: var(--ink-3);
      }
      .dock a:hover {
        color: var(--ink);
        background: var(--paper-2);
      }
      .dock a:active {
        scale: 0.93;
      }
      .dock a[aria-current] {
        background: var(--blue);
        color: var(--on-blue);
      }
      .dock a[aria-current] .k {
        color: var(--on-blue);
        opacity: 0.85;
      }
      @media (max-width: 480px) {
        .dock a {
          padding: 9px 12px;
        }
      }

      /* ---------- twinkling stars around the page ---------- */
      .stars {
        position: fixed;
        inset: 0;
        pointer-events: none;
      }
      .stars span {
        position: absolute;
        width: 14px;
        height: 14px;
        background: var(--hi);
        clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
        animation: twinkle 3.6s ease-in-out infinite;
      }
      @keyframes twinkle {
        0%,
        100% {
          opacity: 0.35;
          scale: 0.8;
          rotate: -8deg;
        }
        50% {
          opacity: 0.9;
          scale: 1.1;
          rotate: 8deg;
        }
      }
      @media (prefers-reduced-motion: reduce) {
        .stars span {
          animation: none;
        }
      }
    </style>
  </head>
  <body>
    <div class="stars" aria-hidden="true">
      <span style="left: 12%; top: 18%"></span>
      <span style="left: 82%; top: 26%; animation-delay: -1.2s"></span>
      <span style="left: 68%; top: 72%; animation-delay: -2.4s; width: 10px; height: 10px"></span>
      <span style="left: 20%; top: 64%; animation-delay: -0.6s; width: 9px; height: 9px"></span>
    </div>
    <main>
      <h1 id="title">首页</h1>
      <p>底部是悬浮的药丸导航:毛玻璃底、当前栏目蓝色高亮、按下有一点回弹。点点看。</p>
    </main>

    <nav class="dock" aria-label="主导航">
      <a href="#home" aria-current="page"><span class="k">01</span>首页</a>
      <a href="#posts"><span class="k">02</span>文章</a>
      <a href="#projects"><span class="k">03</span>项目</a>
      <a href="#about"><span class="k">04</span>关于</a>
    </nav>

    <script>
      // In a real site each link goes to its page and the server marks the current one; here a click
      // just moves the highlight.
      const links = document.querySelectorAll(".dock a");
      for (const link of links) {
        link.addEventListener("click", (event) => {
          event.preventDefault();
          links.forEach((a) => a.removeAttribute("aria-current"));
          link.setAttribute("aria-current", "page");
          document.getElementById("title").textContent = link.lastChild.textContent;
        });
      }
    </script>
  </body>
</html>

Say hi on WeChat

Liu ZhuoQi's WeChat QR code

Scan it in WeChat, or long-press it on your phone

Mention why you're reaching out, or I might miss it.