---
title: "Stamp Button"
summary: "Pressing feels like stamping: it presses down with a slight twist, then a grainy red imprint is left on the paper and slowly fades."
category: "button (Buttons & controls)"
origin: "original"
stack: ["html","css","js"]
tags: ["button","handmade","micro-interaction","spring"]
demo: "https://zhuoqidev.com/design/demos/stamp-button/"
url: "https://zhuoqidev.com/en/design/stamp-button/"
license: "MIT"
added: "2026-10-19"
translation: "https://zhuoqidev.com/design/stamp-button/index.md"
---

# Stamp Button

> Pressing feels like stamping: it presses down with a slight twist, then a grainy red imprint is left on the paper and slowly fades.

## What makes it work

- **It presses down, it doesn't shrink**: on press, `translate: 0 3px` sinks it while the hard `0 3px 0` shadow underneath collapses to 0, so the whole stamp looks pressed onto the paper; adding `rotate: 1deg` makes it feel like a wrist turned it slightly.
- **Springs back**: on release it transitions with a spring curve written in `linear()` (`--spring`), overshooting a little before it stops, with no JS animation library. At the moment of press, `transition-duration` is cut to 0.06 s, which gives a snappy "thwack".
- **The stamp's edge**: `box-shadow: inset 0 0 0 2px` draws a thin light frame inside the button, like the border carved on a stamp face; the text uses a serif (Songti) with wide letter spacing.
- **The imprint left behind**: after a click, an `.imprint` with the same text is inserted below the button, with a bit of randomness in position and angle (±7°), and an SVG made from `feTurbulence` noise is used as a `mask`, so the ink is uneven; after fading out for 1.8 s it removes itself.
- **Three shapes**: solid (primary action), outlined (secondary action) and a round "Read" stamp, all the same class plus a modifier class.

## How to use it

1. Copy the `.stamp` CSS blocks and the `--spring` variable; replace the colors with your own accent color (`--seal`) and the text color on top of it (`--on-seal`).
2. For the imprint effect, also copy `.imprint`, `@keyframes ink` and the script at the bottom; the outer container needs `position: relative; overflow: hidden`, and the imprint lands on this "paper".
3. If you only want the button feel without the imprint: copy only the CSS, with not a line of JS.

## Pitfalls

- The noise image used for `mask` is a data URL generated at runtime and put into a CSS variable; writing it directly in the stylesheet also works, but `#` must be encoded as `%23`.
- `color-mix()` in `box-shadow` needs a browser from after 2023; in older browsers the whole shadow is ignored, and the button still works.
- The stamp button suits ceremonial actions like "confirm / submit"; don't make every button on the page like this.

## Reference implementation (full source)

Live demo: https://zhuoqidev.com/design/demos/stamp-button/. One file: save it as .html and open it.

### index.html

```html
<!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>
```
