Embossing Label Maker with a Letter Wheel That Punches Status Badges into Glossy Tape

Status badges presented as strips of embossed vinyl tape, with the machine that makes them. An orange hand labeller sits on a cream desk, seen from straight above. Its black wheel carries forty characters. For each letter of the word the wheel turns the short way round, a chrome lens shows the character it has stopped on, the cream key dips, and the tape clicks one cell further out of the nose. When the word is finished a toothed blade nips across and the strip slides down to join the pile, landing a few degrees off square. Type up to ten characters, pick one of five tape colours and press Print to make your own. The machine and the pile scale together to fit a phone, while the text field, colour dots and Print button stay full size underneath.

Code

import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";

// The tape cartridges. `tone` is the vinyl, `name` is read out to screen readers.
const tapes = [
  { name: "Green", tone: "#1c7a4c" },
  { name: "Black", tone: "#1d1d20" },
  { name: "Cobalt", tone: "#1f4fcf" },
  { name: "Red", tone: "#c22f2a" },
  { name: "Mustard", tone: "#cf9318" },
];
// Labels already stuck down when the page opens, newest first.
const starting = [
  { text: "IN TRANSIT", tape: 2 },
  { text: "ON HOLD", tape: 4 },
  { text: "DISPATCHED", tape: 1 },
];
// What the machine prints by itself until someone uses it.
const demo = [
  { text: "PAID", tape: 0 },
  { text: "OVERDUE", tape: 3 },
  { text: "PACKED", tape: 2 },
  { text: "FRAGILE", tape: 1 },
];
const BODY = "#e3562c"; // the moulded shell
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&-+/"; // round the wheel, clockwise
const MAX = 10; // longest label, in characters

const SW = 540, SH = 478; // scene, in design pixels
const WX = 404, WY = 150; // wheel centre
const SLOT = 196; // where the tape leaves the nose
const CELL = 16, LEAD = 14, TAPE_H = 28;
const STEP = 360 / CHARS.length;
const widthOf = (text: string) => LEAD * 2 + CELL * text.length;
const clean = (raw: string) => raw.toUpperCase().split("").filter((c) => c === " " || CHARS.includes(c)).join("").slice(0, MAX);
const tilt = [-3.2, 2.4, -1.4, 3.1, -2.2, 1.6];
const drift = [-18, 22, -34, 12, 30, -8];
const teeth = (() => {
  const left: string[] = [], right: string[] = [];
  for (let i = 0; i <= 8; i++) { left.push(`${i % 2 ? 1.6 : 0}px ${(i / 8) * 100}%`); right.unshift(`calc(100% - ${i % 2 ? 0 : 1.6}px) ${(i / 8) * 100}%`); }
  return `polygon(${[...left, ...right].join(",")})`;
})();

function Tape({ text, tone }: { text: string; tone: string }) {
  return (
    <span className="block" style={{ filter: "drop-shadow(0 1px 0.6px rgba(40,24,8,0.5)) drop-shadow(0 4px 5px rgba(40,24,8,0.22))" }}>
      <span
        className="relative flex items-center overflow-hidden"
        style={{ width: widthOf(text), height: TAPE_H, paddingLeft: LEAD, clipPath: teeth, background: `linear-gradient(180deg,rgba(255,255,255,0.34) 0%,rgba(255,255,255,0.1) 26%,rgba(255,255,255,0) 44%,rgba(0,0,0,0) 62%,rgba(0,0,0,0.26) 100%),${tone}` }}
      >
        {text.split("").map((c, i) => (
          <span
            key={i}
            className="relative block text-center font-bold leading-none"
            style={{ width: CELL, fontSize: 15, fontFamily: "ui-rounded,'SF Pro Rounded','Arial Rounded MT Bold','Helvetica Neue',Arial,sans-serif", color: "#f6f2e8", textShadow: "0 0 0.8px rgba(255,255,255,0.9),0.7px 1.1px 0 rgba(0,0,0,0.55),-0.4px -0.5px 0 rgba(255,255,255,0.5),0 0 3px rgba(255,255,255,0.16)" }}
          >
            {c === " " ? " " : c}
          </span>
        ))}
        <span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-white/50" />
        <span aria-hidden="true" className="absolute inset-x-0 bottom-0 h-px bg-black/35" />
        <span aria-hidden="true" className="embossing-label-maker-glare absolute inset-0" />
      </span>
    </span>
  );
}

export default function EmbossingLabelMaker() {
  const reduce = useReducedMotion();
  const [labels, setLabels] = useState(starting.map((l, i) => ({ id: i, text: l.text, tape: l.tape })));
  const [job, setJob] = useState<{ text: string; tape: number } | null>(null);
  const [out, setOut] = useState(0);
  const [turn, setTurn] = useState({ deg: 0, ms: 0 });
  const [lens, setLens] = useState("A");
  const [punch, setPunch] = useState(false);
  const [cut, setCut] = useState(false);
  const [draft, setDraft] = useState("READY");
  const [tape, setTape] = useState(3);
  const [scale, setScale] = useState(1);
  const [said, setSaid] = useState("");

  const root = useRef<HTMLElement>(null);
  const live = useRef({ busy: false, alive: true, touched: false, seen: true, at: 0, deg: 0, id: 100, step: 0 });

  useEffect(() => {
    const node = root.current;
    if (!node) return;
    const fit = () => setScale(Math.max(0.58, Math.min(1.12, (node.clientWidth - 20) / SW)));
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(node);
    const io = new IntersectionObserver(([entry]) => { live.current.seen = entry.isIntersecting; });
    io.observe(node);
    const onMove = (e: PointerEvent) => { const b = node.getBoundingClientRect(); node.style.setProperty("--glare", `${(110 - ((e.clientX - b.left) / b.width) * 120).toFixed(1)}%`); node.dataset.lit = "1"; };
    node.addEventListener("pointermove", onMove);
    return () => { ro.disconnect(); io.disconnect(); node.removeEventListener("pointermove", onMove); };
  }, []);

  const print = async (text: string, tapeIndex: number) => {
    const s = live.current;
    if (s.busy || !text.trim()) return;
    const stick = () => { setLabels((prev) => [{ id: s.id++, text, tape: tapeIndex }, ...prev].slice(0, 4)); setSaid(`Printed ${text} on ${tapes[tapeIndex].name.toLowerCase()} tape`); };
    if (reduce) { stick(); return; }
    s.busy = true;
    const wait = (ms: number) => new Promise<void>((done) => window.setTimeout(done, ms));
    setJob({ text, tape: tapeIndex }); setOut(0);
    await wait(90);
    for (let n = 0; n < text.length && s.alive; n++) {
      const to = CHARS.indexOf(text[n]);
      if (to >= 0 && to !== s.at) {
        // Turn the short way round to the next letter; a longer trip takes a little longer.
        let hop = to - s.at; if (hop > CHARS.length / 2) hop -= CHARS.length; if (hop < -CHARS.length / 2) hop += CHARS.length;
        const ms = 80 + Math.abs(hop) * 7;
        s.at = to; s.deg -= hop * STEP;
        setTurn({ deg: s.deg, ms }); setLens(text[n]);
        await wait(ms + 10);
      }
      setPunch(true); setOut(LEAD + (n + 1) * CELL);
      await wait(80);
      setPunch(false);
      await wait(34);
    }
    setOut(widthOf(text));
    await wait(150);
    setCut(true);
    await wait(100);
    if (!s.alive) return;
    stick(); setJob(null); setOut(0); setCut(false);
    await wait(260);
    s.busy = false;
  };
  const printRef = useRef(print);
  printRef.current = print;

  useEffect(() => {
    const s = live.current;
    s.alive = true;
    if (reduce) return;
    const go = () => { if (s.touched || s.busy || !s.seen || document.hidden) return; const next = demo[s.step++ % demo.length]; printRef.current(next.text, next.tape); };
    const first = window.setTimeout(go, 260);
    const loop = window.setInterval(go, 3800);
    return () => { s.alive = false; window.clearTimeout(first); window.clearInterval(loop); };
  }, [reduce]);

  const text = clean(draft).trim();
  const feed = job ? widthOf(job.text) : 0;
  const BOX = widthOf("W".repeat(MAX));

  return (
    <section
      ref={root}
      className="relative flex min-h-[640px] w-full flex-col items-center justify-center overflow-hidden bg-[#e9e1d0] px-2.5 py-10 sm:min-h-[740px]"
      style={{ backgroundImage: "radial-gradient(90% 70% at 42% 30%,#f6f0e3 0%,#e9e1d0 55%,#d6ccb7 100%)" }}
    >
      <style>{`
        @keyframes embossing-label-maker-glare { 0% { background-position: 120% 0; } 55%, 100% { background-position: -20% 0; } }
        .embossing-label-maker-glare { background: linear-gradient(104deg, rgba(255,255,255,0) 42%, rgba(255,255,255,0.42) 50%, rgba(255,255,255,0) 58%) no-repeat; background-size: 260% 100%; background-position: var(--glare, 120%) 0; animation: embossing-label-maker-glare 4.6s ease-in-out infinite; }
        [data-lit="1"] .embossing-label-maker-glare { animation: none; transition: background-position 0.25s ease-out; }
        @media (prefers-reduced-motion: reduce) { .embossing-label-maker-glare { animation: none; background-position: 46% 0; } }
      `}</style>

      <div style={{ width: SW * scale, height: SH * scale }}>
        <div className="relative" style={{ width: SW, height: SH, transform: `scale(${scale})`, transformOrigin: "0 0" }}>
          {/* shadow of the machine on the desk */}
          <svg aria-hidden="true" className="pointer-events-none absolute left-0 top-0" width={SW} height={SH} viewBox={`0 0 ${SW} ${SH}`} style={{ overflow: "visible" }}>
            <defs>
              <filter id="embossing-label-maker-soft" x="-30%" y="-30%" width="160%" height="170%"><feGaussianBlur stdDeviation="14" /></filter>
              <filter id="embossing-label-maker-tight" x="-30%" y="-30%" width="160%" height="170%"><feGaussianBlur stdDeviation="3.5" /></filter>
            </defs>
            <g fill="#4a2f12">
              <g filter="url(#embossing-label-maker-soft)" opacity="0.42" transform="translate(10 24)"><circle cx={WX} cy={WY} r="104" /><rect x="190" y="104" width="180" height="92" rx="26" /></g>
              <g filter="url(#embossing-label-maker-tight)" opacity="0.5" transform="translate(3 7)"><circle cx={WX} cy={WY} r="103" /><rect x="191" y="105" width="180" height="90" rx="26" /></g>
            </g>
          </svg>

          {/* the tape being fed out, under the nose */}
          <div aria-hidden="true" className="absolute overflow-hidden" style={{ left: SLOT - BOX, top: WY - TAPE_H / 2 - 12, width: BOX, height: TAPE_H + 30 }}>
            {job ? (
              <div className="absolute left-0 top-[12px] motion-reduce:transition-none" style={{ transform: `translateX(${BOX - out}px)`, transition: "transform 95ms cubic-bezier(0.2,1.5,0.4,1)" }}>
                <Tape text={job.text} tone={tapes[job.tape].tone} />
              </div>
            ) : null}
            <div className="absolute right-0 top-[12px] w-4" style={{ height: TAPE_H, background: "linear-gradient(90deg,rgba(40,16,4,0),rgba(40,16,4,0.5))", opacity: feed ? 1 : 0 }} />
          </div>

          {/* printed labels, newest on top */}
          <ul aria-label="Printed labels" className="absolute left-0 top-0 m-0 list-none p-0">
            <AnimatePresence initial={false}>
              {labels.map((l, i) => {
                const w = widthOf(l.text);
                return (
                  <motion.li
                    key={l.id}
                    className="absolute left-0 top-0"
                    style={{ zIndex: 10 - i, transformOrigin: "50% 50%" }}
                    initial={{ x: SLOT - w - 3, y: WY - TAPE_H / 2, rotate: 0, opacity: 1 }}
                    animate={{ x: 266 - w / 2 + drift[l.id % drift.length], y: 294 + i * 45, rotate: tilt[l.id % tilt.length], opacity: 1 }}
                    exit={{ opacity: 0, y: 294 + i * 45 + 18, transition: { duration: 0.25 } }}
                    transition={{ type: "spring", stiffness: 210, damping: 21, mass: 0.9 }}
                  >
                    <span className="sr-only">{l.text}</span>
                    <span aria-hidden="true"><Tape text={l.text} tone={tapes[l.tape].tone} /></span>
                  </motion.li>
                );
              })}
            </AnimatePresence>
          </ul>

          {/* the shell */}
          <svg aria-hidden="true" className="pointer-events-none absolute left-0 top-0" width={SW} height={SH} viewBox={`0 0 ${SW} ${SH}`} style={{ overflow: "visible" }}>
            <defs>
              <filter id="embossing-label-maker-plastic" x="-8%" y="-8%" width="116%" height="116%" colorInterpolationFilters="sRGB">
                <feGaussianBlur in="SourceAlpha" stdDeviation="5.5" result="bump" />
                <feDiffuseLighting in="bump" surfaceScale="7" diffuseConstant="1.12" lightingColor="#fff6ec" result="shade"><feDistantLight azimuth="232" elevation="58" /></feDiffuseLighting>
                <feComposite in="shade" in2="SourceGraphic" operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="lit" />
                <feSpecularLighting in="bump" surfaceScale="7" specularConstant="0.7" specularExponent="26" lightingColor="#ffffff" result="spec"><feDistantLight azimuth="232" elevation="50" /></feSpecularLighting>
                <feComposite in="spec" in2="SourceAlpha" operator="in" result="gloss" />
                <feMerge result="both"><feMergeNode in="lit" /><feMergeNode in="gloss" /></feMerge>
                <feComposite in="both" in2="SourceAlpha" operator="in" />
              </filter>
              <filter id="embossing-label-maker-key" x="-20%" y="-20%" width="140%" height="140%" colorInterpolationFilters="sRGB">
                <feGaussianBlur in="SourceAlpha" stdDeviation="2.6" result="bump" />
                <feDiffuseLighting in="bump" surfaceScale="4" diffuseConstant="1.1" lightingColor="#ffffff" result="shade"><feDistantLight azimuth="232" elevation="60" /></feDiffuseLighting>
                <feComposite in="shade" in2="SourceGraphic" operator="arithmetic" k1="1" k2="0" k3="0" k4="0" result="lit" />
                <feComposite in="lit" in2="SourceAlpha" operator="in" />
              </filter>
              <linearGradient id="embossing-label-maker-steel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#f4f5f6" /><stop offset="0.35" stopColor="#aeb3b8" /><stop offset="0.6" stopColor="#e3e6e9" /><stop offset="1" stopColor="#6d7278" /></linearGradient>
            </defs>
            <path filter="url(#embossing-label-maker-plastic)" fill={BODY} d={`M${WX} ${WY - 104}a104 104 0 1 1 0 208a104 104 0 1 1 0-208ZM216 104h154v92H216a26 26 0 0 1-26-26v-40a26 26 0 0 1 26-26Z`} />
            {/* moulded lettering */}
            <text x="207" y="120.6" fontSize="6.4" fontWeight="700" letterSpacing="1.9" fontFamily="ui-sans-serif,system-ui,Helvetica,Arial,sans-serif" fill="#ffb08e" opacity="0.7">FINCH M-40</text>
            <text x="207" y="120" fontSize="6.4" fontWeight="700" letterSpacing="1.9" fontFamily="ui-sans-serif,system-ui,Helvetica,Arial,sans-serif" fill="#9c3312">FINCH M-40</text>
            {/* the press key in its well */}
            <rect x="228" y="129" width="62" height="50" rx="13" fill="#7d280c" opacity="0.85" />
            <rect x="228" y="130.5" width="62" height="50" rx="13" fill="none" stroke="#ffb693" strokeOpacity="0.5" strokeWidth="1" />
            <g style={{ transform: punch ? "translate(0.8px,1.4px) scale(0.965)" : "none", transformOrigin: "259px 154px", transition: "transform 70ms ease-out" }}>
              <rect x="231" y="132" width="56" height="44" rx="10.5" fill={punch ? "#e2dccd" : "#f3eee1"} filter="url(#embossing-label-maker-key)" />
              <path d="M252 150.5l7 7 7-7" fill="none" stroke="#8c8674" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
              <path d="M252 151.4l7 7 7-7" fill="none" stroke="#ffffff" strokeOpacity="0.8" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
            </g>
            {/* window onto the cartridge */}
            <circle cx="211" cy={WY} r="8.6" fill="#6d2209" />
            <circle cx="211" cy={WY + 0.9} r="9" fill="none" stroke="#ffb693" strokeOpacity="0.5" strokeWidth="1" />
            <circle cx="211" cy={WY} r="6.8" fill={tapes[job ? job.tape : tape].tone} style={{ transition: "fill 200ms" }} />
            <ellipse cx="208.8" cy={WY - 2.6} rx="3.4" ry="2" fill="#ffffff" opacity="0.55" />
            {/* the well the wheel turns in */}
            <circle cx={WX} cy={WY} r="95.5" fill="#571a06" />
            <circle cx={WX} cy={WY + 1.2} r="96" fill="none" stroke="#ffb693" strokeOpacity="0.55" strokeWidth="1.2" />
            {/* serrated blade at the mouth */}
            <g style={{ transform: cut ? "translateX(-3px)" : "none", transition: "transform 60ms ease-out" }}>
              <path fill="url(#embossing-label-maker-steel)" stroke="#4a4e53" strokeWidth="0.5" d={`M194 ${WY - 21}h-4l-2.4 3 2.4 3-2.4 3 2.4 3-2.4 3 2.4 3-2.4 3 2.4 3-2.4 3 2.4 3-2.4 3 2.4 3-2.4 3 2.4 3h4Z`} />
            </g>
          </svg>

          {/* the letter wheel */}
          <div aria-hidden="true" className="absolute" style={{ left: WX - 92, top: WY - 92, width: 184, height: 184, transform: punch ? "scale(0.986)" : "none", transition: "transform 70ms ease-out" }}>
            <div className="absolute inset-0 rounded-full" style={{ background: "radial-gradient(circle at 50% 50%,#2b2b2f 0%,#1d1d20 58%,#0e0e10 100%)", boxShadow: "0 5px 9px rgba(20,6,0,0.55), 0 1px 0 rgba(255,255,255,0.25) inset, 0 -2px 3px rgba(0,0,0,0.8) inset" }} />
            <div className="absolute inset-0 rounded-full motion-reduce:transition-none" style={{ transform: `rotate(${turn.deg}deg)`, transition: `transform ${turn.ms}ms cubic-bezier(0.3,1.35,0.5,1)` }}>
              <div className="absolute inset-0 rounded-full" style={{ background: "repeating-conic-gradient(#08080a 0deg 2.1deg,#45454a 2.1deg 3.1deg,#1a1a1d 3.1deg 4.5deg)", WebkitMaskImage: "radial-gradient(circle,transparent 82px,#000 83px,#000 91px,transparent 92px)", maskImage: "radial-gradient(circle,transparent 82px,#000 83px,#000 91px,transparent 92px)" }} />
              {CHARS.split("").map((c, i) => (
                <span key={c} className="absolute left-1/2 top-1/2 block w-[14px] text-center font-bold leading-none text-[#f1ede2]" style={{ fontSize: 10.5, marginLeft: -7, marginTop: -5.5, transform: `rotate(${i * STEP}deg) translateY(-70px)`, fontFamily: "ui-rounded,'SF Pro Rounded','Arial Rounded MT Bold','Helvetica Neue',Arial,sans-serif" }}>{c}</span>
              ))}
              {[0, 120, 240].map((d) => (
                <span key={d} className="absolute left-1/2 top-1/2 block h-[5px] w-[5px] rounded-full bg-[#f1ede2]" style={{ marginLeft: -2.5, marginTop: -2.5, transform: `rotate(${d}deg) translateY(-52px)`, boxShadow: "0 1px 0 rgba(0,0,0,0.6)" }} />
              ))}
            </div>
            {/* light on the wheel does not turn with it */}
            <div className="absolute inset-0 rounded-full" style={{ background: "conic-gradient(from 200deg,rgba(255,255,255,0.3),rgba(255,255,255,0) 11%,rgba(255,255,255,0) 38%,rgba(255,255,255,0.16) 50%,rgba(255,255,255,0) 62%,rgba(255,255,255,0) 89%,rgba(255,255,255,0.3))", mixBlendMode: "screen" }} />
            <div className="absolute rounded-full" style={{ inset: 11, boxShadow: "0 0 0 1px rgba(255,255,255,0.1), 0 0 0 1px rgba(0,0,0,0.6) inset" }} />
            <div className="absolute rounded-full" style={{ inset: 36, boxShadow: "0 0 0 1px rgba(255,255,255,0.08)" }} />
            {/* hub cap with a thumb dish */}
            <div className="absolute rounded-full" style={{ inset: 49, background: `radial-gradient(circle at 36% 28%,#ff9a70 0%,${BODY} 46%,#a83815 100%)`, boxShadow: "0 4px 7px rgba(0,0,0,0.7), 0 1px 0 rgba(255,255,255,0.55) inset, 0 -2px 3px rgba(90,24,0,0.6) inset" }} />
            <div className="absolute rounded-full" style={{ inset: 62, background: "radial-gradient(circle at 64% 72%,#ff8e63 0%,#d94c22 48%,#9a3010 100%)", boxShadow: "0 1.5px 2px rgba(60,14,0,0.7) inset, 0 -1px 0 rgba(255,255,255,0.4) inset, 0 1px 0 rgba(255,255,255,0.35)" }} />
          </div>

          {/* reading lens over the chosen letter */}
          <div aria-hidden="true" className="absolute" style={{ left: WX - 19, top: WY - 70 - 21, width: 38, height: 38 }}>
            <div className="absolute left-1/2 top-[-17px] h-[22px] w-[14px] -translate-x-1/2 rounded-[3px]" style={{ background: "linear-gradient(90deg,#8d9197,#f5f6f7 38%,#b9bdc2 62%,#70757b)", boxShadow: "0 2px 3px rgba(0,0,0,0.45)" }} />
            <div className="absolute inset-0 rounded-full" style={{ background: "conic-gradient(from 210deg,#ffffff,#8a8f95 18%,#e9ebee 40%,#5f646a 62%,#d7dadd 82%,#ffffff)", boxShadow: "0 4px 6px rgba(0,0,0,0.55), 0 1px 1px rgba(0,0,0,0.5)" }} />
            <div className="absolute flex items-center justify-center overflow-hidden rounded-full font-bold text-[#f6f2e8]" style={{ inset: 4, fontSize: 19, lineHeight: 1, background: "radial-gradient(circle at 50% 40%,#3a3a3f,#101012 78%)", boxShadow: "0 0 5px rgba(0,0,0,0.9) inset", fontFamily: "ui-rounded,'SF Pro Rounded','Arial Rounded MT Bold','Helvetica Neue',Arial,sans-serif" }}>
              {lens}
              <span className="absolute inset-0 rounded-full" style={{ background: "radial-gradient(60% 46% at 34% 22%,rgba(255,255,255,0.6),rgba(255,255,255,0) 70%),radial-gradient(70% 60% at 70% 96%,rgba(255,255,255,0.2),rgba(255,255,255,0) 70%)" }} />
            </div>
          </div>
        </div>
      </div>

      <form
        className="relative mt-3 flex w-full max-w-[460px] flex-wrap items-center justify-center gap-x-3 gap-y-3"
        onSubmit={(e) => { e.preventDefault(); live.current.touched = true; print(text, tape); }}
      >
        <label htmlFor="embossing-label-maker-text" className="sr-only">Label text, up to {MAX} characters</label>
        <input
          id="embossing-label-maker-text"
          value={draft}
          onChange={(e) => { live.current.touched = true; setDraft(clean(e.target.value)); }}
          onFocus={() => { live.current.touched = true; }}
          maxLength={MAX}
          autoComplete="off"
          spellCheck={false}
          className="h-11 w-[150px] rounded-full border border-[#b9ad95] bg-[#fbf8f1] px-4 font-mono text-[14px] font-semibold uppercase tracking-[0.14em] text-[#2a2520] outline-none placeholder:text-[#6f6656] focus-visible:ring-2 focus-visible:ring-[#b23c1a]"
          placeholder="LABEL"
        />
        <div role="radiogroup" aria-label="Tape colour" className="flex items-center">
          {tapes.map((t, i) => (
            <button
              key={t.name}
              type="button"
              role="radio"
              aria-checked={tape === i}
              aria-label={`${t.name} tape`}
              onClick={() => { live.current.touched = true; setTape(i); }}
              className="flex h-10 w-8 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-[#b23c1a]"
            >
              <span className="block h-[22px] w-[22px] rounded-full transition-transform duration-200 motion-reduce:transition-none" style={{ background: `linear-gradient(160deg,rgba(255,255,255,0.4),rgba(255,255,255,0) 50%),${t.tone}`, boxShadow: tape === i ? "0 0 0 2px #e9e1d0, 0 0 0 4px #2a2520" : "0 1px 2px rgba(40,24,8,0.5)", transform: tape === i ? "scale(0.86)" : "none" }} />
            </button>
          ))}
        </div>
        <button
          type="submit"
          disabled={!text}
          className="h-11 rounded-full bg-[#2a2520] px-6 font-mono text-[12px] font-semibold uppercase tracking-[0.2em] text-[#fbf8f1] outline-none transition-transform duration-150 hover:bg-black focus-visible:ring-2 focus-visible:ring-[#b23c1a] focus-visible:ring-offset-2 focus-visible:ring-offset-[#e9e1d0] active:scale-95 disabled:opacity-50 motion-reduce:transition-none"
        >
          Print
        </button>
      </form>
      <p className="sr-only" aria-live="polite">{said}</p>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/embossing-label-maker-with-a-letter-wheel-that-punches-status-badges-into-glossy-tape.json

When to use it

How to customize it

Accessibility

Questions

How do I use only the badge without the machine?

Copy the `Tape` function with `teeth`, `widthOf`, `CELL`, `LEAD` and `TAPE_H`. It takes a `text` and a `tone` and returns one strip. The glare class in the style block is optional and can be dropped.

How is the raised lettering done?

Each character is cream text with four text shadows: a tight white halo for the stretched vinyl, a dark offset below and right, a pale offset above and left, and a wide soft glow. The strip itself is one gradient that is light at the top and dark at the bottom.

Why does the tape come out to the left?

So the word reads the right way round as it appears. The first letter punched is the first to leave the machine, and it has to end up on the left of the strip.

What makes the orange shell look moulded and not flat?

It is one SVG path with a lighting filter. The filter blurs the outline into a height map, then lights it from the upper left with a diffuse pass and a specular pass, which rounds every edge and adds the glossy streaks.

Which characters can be printed?

Capital letters, digits, and the ampersand, hyphen, plus and slash on the wheel, along with spaces. Anything else typed in the field is removed, lower case is raised to capitals, and the length is capped by `MAX`.

Related: Index Card Checklist Where a Pencil Draws Each Tick in Graphite and Its Eraser Rubs It Out, Tear-Off Desk Calendar with Pages That Peel, Curl and Flutter Away.

More alerts and feedback or all free UI components. Start for free to have a full site built around it.

More in Feedback

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap