Cheque

No dependencies
Code format
React + Tailwind
// A fine wave repeated across the paper, the anti-forgery pattern banks print.
const GUILLOCHE = Array.from({ length: 5 }, (_, k) => {
  let d = `M0 ${16 + k * 2}`;
  for (let x = 0; x <= 200; x += 5) d += ` L${x} ${(16 + k * 2 + Math.sin((x + k * 9) / 6.5) * (7 - k)).toFixed(1)}`;
  return d;
});
const SIGNATURE = 'M4 30C10 12 15 4 17 8C19 14 13 30 16 31C20 28 24 14 28 12C31 12 27 28 30 29C34 28 36 20 40 19C43 20 39 28 43 28C47 27 50 17 53 8C55 3 57 6 55 14C53 22 52 28 55 28C59 27 62 20 64 21C66 24 64 29 67 28C72 26 74 18 80 10C76 22 76 30 80 29C84 26 88 22 94 22C104 22 112 20 124 15';

const css = `
  .cb-cheque { --paper: light-dark(#f3f4f7, #e5e6e9); --print: light-dark(#373a40, #121417); --soft: color-mix(in oklab, var(--print) 74%, var(--paper)); --flag: light-dark(#373a40, #232529); --flag-ink: light-dark(#fbfcfd, #f1f2f3); --hole: light-dark(#6d7179, #121417); flex: none; position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; font-weight: 500; cursor: pointer; -webkit-user-select: none; user-select: none; }
  .cb-cheque__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .cb-cheque__art { position: relative; display: block; width: 268px; height: 152px; filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.34)); transform: rotate(-1.4deg); }
  .cb-cheque__input:focus-visible + .cb-cheque__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; border-radius: 6px; }
  .cb-cheque__sheet { position: absolute; inset: 0; overflow: hidden; border-radius: 3px; background: var(--paper); color: var(--print); font-family: Georgia, 'Times New Roman', serif; }
  .cb-cheque__wave { position: absolute; right: 0; top: 0; width: 212px; height: 100%; opacity: 0.2; fill: none; stroke: var(--print); stroke-width: 0.55; }
  /* The tear-off stub, split from the cheque by a row of real holes. */
  .cb-cheque__holes { position: absolute; left: 50px; top: 0; bottom: 0; width: 4px; background: radial-gradient(circle at 2px 3px, var(--hole) 1.3px, transparent 1.6px) 0 0 / 4px 6px; }
  .cb-cheque__stub { position: absolute; left: 6px; top: 8px; width: 40px; font-size: 6.5px; line-height: 1.5; }
  .cb-cheque__stub b { display: block; font-size: 10px; }
  .cb-cheque__bank { position: absolute; left: 62px; top: 8px; font-size: 11.5px; font-weight: 700; letter-spacing: -0.01em; }
  .cb-cheque__num { position: absolute; right: 8px; top: 9px; font: 700 8.5px 'Courier New', Courier, monospace; }
  .cb-cheque__row { position: absolute; left: 62px; right: 8px; display: flex; align-items: flex-end; gap: 5px; font-size: 6.5px; letter-spacing: 0.02em; text-transform: uppercase; }
  .cb-cheque__row i { flex: 1; min-width: 0; padding: 0 3px 1px; border-bottom: 1px solid var(--print); font: italic 700 11px/1.1 Georgia, 'Times New Roman', serif; letter-spacing: 0; text-transform: none; white-space: nowrap; overflow: hidden; }
  .cb-cheque__amount { position: absolute; right: 8px; top: 26px; padding: 2px 5px; border: 1px solid var(--print); background: var(--paper); font: 700 11px 'Courier New', Courier, monospace; }
  .cb-cheque__line { position: absolute; left: 150px; right: 10px; top: 110px; height: 1px; background: var(--print); }
  .cb-cheque__sign-label { position: absolute; right: 10px; top: 112px; font-size: 6px; letter-spacing: 0.04em; text-transform: uppercase; }
  .cb-cheque__micr { position: absolute; left: 62px; bottom: 7px; font: 700 8.5px 'Courier New', Courier, monospace; letter-spacing: 0.08em; }
  .cb-cheque__pen { position: absolute; left: 134px; top: 82px; width: 130px; height: 36px; overflow: visible; fill: none; stroke: var(--print); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
  .cb-cheque__ink { stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: opacity 0.28s, stroke-dashoffset 0s 0.28s; }
  .cb-cheque__input:checked + .cb-cheque__art .cb-cheque__ink { stroke-dashoffset: 0; opacity: 1; transition: stroke-dashoffset 0.85s cubic-bezier(0.35, 0.1, 0.6, 1) 0.18s, opacity 0s; }
  .cb-cheque__flag { position: absolute; left: 86px; top: 100px; display: flex; align-items: center; height: 19px; padding: 0 13px 0 8px; background: var(--flag); color: var(--flag-ink); clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%); font: 700 9.5px system-ui, sans-serif; white-space: nowrap; transform-origin: 0 50%; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1), opacity 0.2s; }
  .cb-cheque:hover .cb-cheque__flag { transform: translateX(4px); }
  .cb-cheque__input:checked + .cb-cheque__art .cb-cheque__flag { transform: translate(-28px, 34px) rotate(-30deg) scale(0.9); opacity: 0; transition: transform 0.36s ease-in, opacity 0.2s 0.14s; }
  .cb-cheque__stamp { position: absolute; left: 68px; top: 88px; padding: 3px 9px; border: 2.5px solid var(--print); border-radius: 4px; color: var(--print); font: 900 18px/1 'Arial Black', Impact, system-ui, sans-serif; letter-spacing: 0.08em; opacity: 0; transform: rotate(-11deg) scale(1.9); transition: opacity 0.12s, transform 0.12s; }
  .cb-cheque__input:checked + .cb-cheque__art .cb-cheque__stamp { opacity: 0.78; transform: rotate(-11deg); transition: opacity 0.06s 1.02s, transform 0.16s cubic-bezier(0.5, 0, 1, 0.6) 1.02s; }
  .cb-cheque__input:checked + .cb-cheque__art { animation: cb-cheque-thud 0.3s cubic-bezier(0.3, 1.6, 0.5, 1) 1.17s; }
  @keyframes cb-cheque-thud {
    0% { transform: rotate(-1.4deg) translateY(3px) scale(0.99); }
    100% { transform: rotate(-1.4deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .cb-cheque *, .cb-cheque__input:checked + .cb-cheque__art, .cb-cheque__input:checked + .cb-cheque__art * { animation: none !important; transition: none !important; }
  }
`;

export function ChequeCheckbox() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="cb-cheque">
        <input className="cb-cheque__input" type="checkbox" />
        <span className="cb-cheque__art" aria-hidden="true">
          <span className="cb-cheque__sheet">
            <svg className="cb-cheque__wave" viewBox="0 0 212 152" preserveAspectRatio="none">
              {[0, 34, 68, 102].map((y) => (
                <g key={y} transform={`translate(0 ${y})`}>
                  {GUILLOCHE.map((d, i) => (
                    <path key={i} d={d} />
                  ))}
                </g>
              ))}
            </svg>
            <span className="cb-cheque__stub">
              <b>No. 481</b>
              To M. Holt
              <br />
              Invoice 22
              <br />
              $1,200.00
            </span>
            <span className="cb-cheque__holes" />
            <span className="cb-cheque__bank">First Motion Bank</span>
            <span className="cb-cheque__num">No. 481</span>
            <span className="cb-cheque__amount">$1,200.00</span>
            <span className="cb-cheque__row" style={{ top: 46, right: 8 }}>
              Pay <i>Mira Holt</i>
            </span>
            <span className="cb-cheque__row" style={{ top: 66 }}>
              <i>One thousand two hundred only</i>
            </span>
            <span className="cb-cheque__line" />
            <span className="cb-cheque__sign-label">Authorised signature</span>
            <span className="cb-cheque__micr">⑆021000⑆ 8841 0327⑈ 481</span>
            <svg className="cb-cheque__pen" viewBox="0 0 130 36">
              <path className="cb-cheque__ink" pathLength={1} d={SIGNATURE} />
            </svg>
            <span className="cb-cheque__stamp">PAID</span>
          </span>
          <span className="cb-cheque__flag">Sign here</span>
        </span>
        Sign and pay
      </label>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/checkbox-cheque
Source on GitHub

About this component

The other kind of check. A proper paper cheque with fine print and perforations: tick it, the Sign here flag peels away, a signature writes itself and a PAID stamp lands.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, draw, keyframe
Techniques
stroke dashoffset, css transition, keyframes, svg, css variables
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox inside a <label>, so "Sign and pay" is its name. The cheque is aria-hidden and keyboard focus outlines it. Under prefers-reduced-motion the signature and the stamp appear at once.

More checkboxes

See all
Door hanger
Hatch
Icing
Fortune ball