卓琪的开发笔记zhuoqidev.comRSSllms.txt

← 全部灵感~/design/stamp-button

按钮与控件原创

印章按钮

按下去像盖章:往下一压、轻轻一拧,松手后纸上留下一枚带颗粒感、慢慢淡掉的红印。

html · css · jsMIT收录于 #按钮#手作感#微交互#弹簧

在线演示新窗口打开 ↗

看点

  • 压下去,不是缩小:按下时 translate: 0 3px 往下沉、底下那条 0 3px 0 的硬阴影同时收成 0,看起来是整块印章贴到了纸上;再加 rotate: 1deg,像手腕带了一下。
  • 弹回来:松手用 linear() 写的弹簧曲线过渡(--spring),会冲过头一点再停,不用 JS 动画库。按下那一刻把 transition-duration 压到 0.06 秒,手感是「啪」一下。
  • 印章的边:box-shadow: inset 0 0 0 2px 在按钮里面画一圈浅色细框,像刻出来的印面边框;字用宋体加宽字距。
  • 留下的印:点击后在按钮下方插一个同样文字的 .imprint,位置和角度各带一点随机(±7°),用 feTurbulence 噪声做的 SVG 当 mask,墨色就不均匀;1.8 秒淡出后自己删掉。
  • 三种形状:实心(主操作)、描边(次要操作)、圆形「已阅」,都是同一个类加修饰类。

怎么用

  1. 拷 .stamp 那几段 CSS 和 --spring 变量;颜色换成你自己的强调色(--seal)和它上面的字色(--on-seal)。
  2. 想要落印效果,再拷 .imprint、@keyframes ink 和底部那段脚本;外层容器要 position: relative; overflow: hidden,印就落在这张「纸」上。
  3. 只要按钮手感、不要印:只拷 CSS,一行 JS 都不用。

坑

  • mask 用的噪声图是运行时生成的 data URL,放进 CSS 变量里;直接写在样式表里也行,但要把 # 编成 %23。
  • box-shadow 里的 color-mix() 需要 2023 年后的浏览器;老浏览器整条阴影失效,按钮仍能用。
  • 印章按钮适合「确认 / 提交」这类有仪式感的动作,别把页面上所有按钮都做成这样。
$给 Agent 读这篇:index.md

源码 MIT 开源,拿去改。

<!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;
        --card: #fffdf8;
        --ink: #1c2230;
        --ink-2: #4a5163;
        --ink-3: #636877;
        --line: #e6dfd0;
        --dot: #d8d0bf;
        --seal: #c0391f;
        --on-seal: #fff8ef;
        --blue: #2f5bd3;
        /* A damped spring sampled into linear(): overshoots about 16%, 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-seal: "Songti SC", STSong, "Noto Serif CJK SC", "Source Han Serif SC", SimSun, serif;
        color-scheme: light;
      }
      @media (prefers-color-scheme: dark) {
        :root {
          --paper: #14161b;
          --card: #1e2129;
          --ink: #ece7dc;
          --ink-2: #bab6ac;
          --ink-3: #959aa8;
          --line: #31353f;
          --dot: #2c3039;
          --seal: #f0694e;
          --on-seal: #1b0e0b;
          --blue: #8eaaff;
          color-scheme: dark;
        }
      }
      * { box-sizing: border-box; }
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        padding: 32px 16px;
        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);
      }
      .sheet {
        position: relative;
        width: min(520px, 100%);
        padding: 28px 28px 32px;
        background: var(--card);
        border: 1px solid var(--line);
        border-radius: 14px;
        box-shadow: 0 10px 28px -14px rgba(28, 34, 48, 0.28);
        overflow: hidden; /* imprints that land near the edge stay on the paper */
      }
      .sheet h1 { margin: 0 0 4px; font: 700 20px/1.4 var(--f-seal); }
      .sheet p { margin: 0 0 22px; color: var(--ink-2); font-size: 14px; }
      .row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }

      /* ---------- the stamp button ---------- */
      .stamp {
        position: relative;
        z-index: 1;
        padding: 11px 22px;
        border: 0;
        border-radius: 6px;
        background: var(--seal);
        color: var(--on-seal);
        font: 700 16px/1 var(--f-seal);
        letter-spacing: 0.18em;
        cursor: pointer;
        /* The inner ring is the carved border of a seal. */
        box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--on-seal) 55%, transparent), 0 3px 0 color-mix(in srgb, var(--seal) 55%, #000);
        transition: translate 0.5s var(--spring), rotate 0.5s var(--spring), box-shadow 0.15s;
      }
      .stamp:hover { rotate: -1.5deg; }
      /* Pressed: pushed down onto the paper, its shadow gone, a slight twist like a real hand. */
      .stamp:active {
        translate: 0 3px;
        rotate: 1deg;
        transition-duration: 0.06s;
        box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--on-seal) 55%, transparent), 0 0 0 color-mix(in srgb, var(--seal) 55%, #000);
      }
      .stamp:focus-visible { outline: 2px solid var(--blue); outline-offset: 4px; }
      .stamp.ghost {
        background: transparent;
        color: var(--seal);
        box-shadow: inset 0 0 0 2px var(--seal), 0 3px 0 color-mix(in srgb, var(--seal) 30%, transparent);
      }
      .stamp.ghost:active { box-shadow: inset 0 0 0 2px var(--seal), 0 0 0 transparent; }
      .stamp.round {
        width: 64px;
        height: 64px;
        padding: 0;
        border-radius: 50%;
        letter-spacing: 0;
        font-size: 15px;
        line-height: 1.1;
      }

      /* ---------- the imprint left on the paper ---------- */
      .imprint {
        position: absolute;
        z-index: 0;
        pointer-events: none;
        display: grid;
        place-items: center;
        border: 2px solid var(--seal);
        color: var(--seal);
        font: 700 16px/1 var(--f-seal);
        letter-spacing: 0.18em;
        /* Uneven ink: an SVG noise mask eats random specks out of the print. */
        -webkit-mask: var(--grain) center / 120px 120px;
        mask: var(--grain) center / 120px 120px;
        animation: ink 1.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
      }
      .imprint.round { border-radius: 50%; letter-spacing: 0; font-size: 15px; line-height: 1.1; text-align: center; }
      @keyframes ink {
        0% { opacity: 0; scale: 1.08; }
        8% { opacity: 0.9; scale: 1; }
        60% { opacity: 0.75; }
        100% { opacity: 0; }
      }
      @media (prefers-reduced-motion: reduce) {
        .stamp, .stamp:active { transition: none; translate: none; rotate: none; }
        .imprint { animation-duration: 0.01s; }
      }
      .hint { margin-top: 64px; font-size: 12px; color: var(--ink-3); }
    </style>
  </head>
  <body>
    <main class="sheet" id="sheet">
      <h1>确认无误,盖个章</h1>
      <p>按下去像真的印章:往下一压、轻轻一拧,松手后纸上留下一枚会慢慢淡掉的印。</p>
      <div class="row">
        <button class="stamp" type="button">提交</button>
        <button class="stamp ghost" type="button">存草稿</button>
        <button class="stamp round" type="button">已<br />阅</button>
      </div>
      <p class="hint">每次落印的角度、位置都有一点随机,像手盖的。</p>
    </main>

    <script>
      // A noise texture for the uneven ink, built once as an SVG data URL (white = ink stays).
      const grain = `url("data:image/svg+xml,${encodeURIComponent(
        `<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' seed='7'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 -2.2 1.6'/></filter><rect width='120' height='120' filter='url(%23n)'/></svg>`,
      )}")`;
      document.documentElement.style.setProperty("--grain", grain);

      const sheet = document.getElementById("sheet");
      const still = matchMedia("(prefers-reduced-motion: reduce)");
      for (const btn of document.querySelectorAll(".stamp")) {
        btn.addEventListener("click", () => {
          if (still.matches) return;
          // The print lands where the button is, a little off and a little turned.
          const s = sheet.getBoundingClientRect();
          const b = btn.getBoundingClientRect();
          const mark = document.createElement("span");
          mark.className = "imprint" + (btn.classList.contains("round") ? " round" : "");
          mark.innerHTML = btn.innerHTML;
          Object.assign(mark.style, {
            left: `${b.left - s.left + (Math.random() - 0.5) * 10}px`,
            top: `${b.top - s.top + b.height * 0.9 + Math.random() * 18}px`,
            width: `${b.width}px`,
            height: `${b.height}px`,
            rotate: `${(Math.random() - 0.5) * 14}deg`,
          });
          sheet.append(mark);
          mark.addEventListener("animationend", () => mark.remove());
        });
      }
    </script>
  </body>
</html>

来加个微信吧

刘卓琪的微信二维码

微信扫码,或在手机上长按识别

加好友请备注来意,不然可能会漏掉哦~