Punch Card
import { useId, useState, type CSSProperties } from 'react';
const COLS = 24;
const X0 = 16;
const CW = 9.4;
const Y0 = 22;
const RH = 7.4;
// Rows top to bottom: the 12 and 11 zones, then 0 to 9.
const colX = (c: number) => +(X0 + c * CW).toFixed(1);
const row = (r: number | 'z12' | 'z11') => (r === 'z12' ? 0 : r === 'z11' ? 1 : r + 2);
// The real card code: digits punch their row; letters add a zone punch on top.
function holes(ch: string): number[] | null {
if (ch === ' ') return [];
if (/[0-9]/.test(ch)) return [row(Number(ch))];
const c = ch.charCodeAt(0);
if (ch >= 'A' && ch <= 'I') return [row('z12'), row(c - 64)];
if (ch >= 'J' && ch <= 'R') return [row('z11'), row(c - 73)];
if (ch >= 'S' && ch <= 'Z') return [row(0), row(c - 81)];
return { '-': [row('z11')], '&': [row('z12')], '/': [row(0), row(1)], '.': [row('z12'), row(3), row(8)] }[ch] ?? null;
}
const css = `
.field-punch-card { --card: light-dark(#dbdde1, #e5e6e9); --ink: light-dark(#50535b, #232529); --print: light-dark(#373a40, #121417); display: grid; justify-items: center; gap: 10px; }
.field-punch-card__card { position: relative; display: block; width: 258px; height: 114px; background: var(--card); clip-path: polygon(11px 0, 100% 0, 100% 100%, 0 100%, 0 11px); filter: drop-shadow(0 6px 8px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.field-punch-card__wrap { position: relative; padding-top: 10px; cursor: text; }
/* The keypunch line: a real field, so the caret, selection and editing are the browser's own. */
.field-punch-card__field { display: flex; align-items: center; gap: 8px; box-sizing: border-box; width: 258px; height: 36px; padding: 0 12px; border-radius: 8px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)); cursor: text; transition: box-shadow 0.2s; }
.field-punch-card__field:has(.field-punch-card__input:focus) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)); }
.field-punch-card__field:has(.field-punch-card__input:focus-visible) { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-punch-card__prompt { color: light-dark(#6d7179, #7d8087); font: 600 13px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
.field-punch-card__input { flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); caret-color: light-dark(#373a40, #f1f2f3); font: 600 14px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.06em; text-transform: uppercase; outline: none; }
.field-punch-card__input::placeholder { color: light-dark(#6d7179, #7d8087); text-transform: none; letter-spacing: 0; font-weight: 500; }
.field-punch-card__col { color: light-dark(#5f626a, #a7abb2); font: 500 11px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; white-space: nowrap; font-variant-numeric: tabular-nums; }
.field-punch-card__col b { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.field-punch-card__digits { position: absolute; left: ${X0 + 3}px; color: var(--ink); font: 500 5.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: ${(CW - 3.3).toFixed(2)}px; white-space: nowrap; pointer-events: none; }
.field-punch-card__band { position: absolute; left: 0; top: 0; bottom: 0; width: ${CW}px; background: color-mix(in oklab, var(--print) 7%, transparent); transition: transform 0.25s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-punch-card__printed { position: absolute; top: 7px; width: ${CW}px; color: var(--print); font: 700 7.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; animation: field-punch-card-print 0.3s ease-out 0.12s both; }
@keyframes field-punch-card-print { from { opacity: 0; transform: translateY(-3px); } }
/* A hole shows whatever is behind the card; its chad drops out of it. */
.field-punch-card__hole { position: absolute; width: 4.2px; height: 6px; border-radius: 0.8px; background: light-dark(#fbfcfd, #121417); box-shadow: inset 0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); animation: field-punch-card-punch 0.18s cubic-bezier(0.5, 0, 0.5, 1.6) both; }
@keyframes field-punch-card-punch { from { transform: scale(0.2); } }
.field-punch-card__chad { position: absolute; z-index: 2; width: 4.2px; height: 6px; border-radius: 0.8px; background: var(--card); box-shadow: 0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); opacity: 0; pointer-events: none; animation: field-punch-card-chad 0.8s cubic-bezier(0.4, 0, 0.9, 0.6) forwards; }
@keyframes field-punch-card-chad { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translate(var(--dx), 70px) rotate(var(--r)); } }
/* The punch head rides above the card to the next column. */
.field-punch-card__head { position: absolute; left: 0; top: 0; width: 9px; height: 8px; border-radius: 2px 2px 0 0; background: light-dark(#5f626a, #a7abb2); transition: transform 0.25s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-punch-card__head::after { content: ''; position: absolute; left: 3px; bottom: -3px; width: 3px; height: 3px; background: light-dark(#5f626a, #a7abb2); }
.field-punch-card:has(.field-punch-card__input:focus) .field-punch-card__head { background: light-dark(#373a40, #f1f2f3); }
.field-punch-card:has(.field-punch-card__input:focus) .field-punch-card__head::after { background: light-dark(#373a40, #f1f2f3); }
.field-punch-card[data-bad='0'] { animation: field-punch-card-nope-a 0.3s; }
.field-punch-card[data-bad='1'] { animation: field-punch-card-nope-b 0.3s; }
@keyframes field-punch-card-nope-a { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@keyframes field-punch-card-nope-b { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@media (prefers-reduced-motion: reduce) {
.field-punch-card * { animation: none !important; transition: none !important; }
.field-punch-card__chad { display: none; }
}
`;
export function Preview() {
const id = useId();
const [text, setText] = useState('RUN MOVIN');
const [caret, setCaret] = useState(text.length);
const [bad, setBad] = useState(0);
const up = text.toUpperCase();
const onChange = (raw: string) => {
// Characters the card has no code for are refused, with a rattle.
if ([...raw.toUpperCase()].some((c) => holes(c) === null)) return setBad((b) => b + 1);
setText(raw);
};
// The punch head waits over the column the caret is in.
const track = (e: { currentTarget: HTMLInputElement }) => setCaret(e.currentTarget.selectionStart ?? e.currentTarget.value.length);
const col = Math.min(caret, COLS - 1);
return (
<>
<style href="kit-inputs-punch-card" precedence="kit">
{css}
</style>
<div className="field-punch-card" data-bad={bad ? bad % 2 : undefined}>
<label className="field-punch-card__wrap" htmlFor={id}>
<span className="field-punch-card__head" style={{ transform: `translateX(${colX(col)}px)` }} aria-hidden="true" />
<span className="field-punch-card__card" aria-hidden="true">
<span className="field-punch-card__band" style={{ transform: `translateX(${colX(col)}px)` }} />
{Array.from({ length: 10 }, (_, d) => (
<span key={d} className="field-punch-card__digits" style={{ top: Y0 + (d + 2) * RH + 1 }}>
{String(d).repeat(COLS)}
</span>
))}
{[...up].map((ch, c) => (
<span key={`${c}${ch}`}>
<span className="field-punch-card__printed" style={{ left: colX(c) }}>
{ch}
</span>
{(holes(ch) ?? []).map((r) => (
<span key={r}>
<i className="field-punch-card__hole" style={{ left: +(colX(c) + 2.6).toFixed(1), top: +(Y0 + r * RH + 0.7).toFixed(1) }} />
<i
className="field-punch-card__chad"
style={{ left: +(colX(c) + 2.6).toFixed(1), top: +(Y0 + r * RH + 0.7).toFixed(1), '--dx': `${((c * 7 + r * 3) % 9) - 4}px`, '--r': `${((c * 31 + r * 17) % 300) - 150}deg` } as CSSProperties}
/>
</span>
))}
</span>
))}
</span>
</label>
<label className="field-punch-card__field">
<span className="field-punch-card__prompt" aria-hidden="true">
>
</span>
<input
id={id}
className="field-punch-card__input"
maxLength={COLS}
autoComplete="off"
spellCheck={false}
placeholder="Type a command"
aria-label="Command"
value={text}
onChange={(e) => {
onChange(e.target.value);
track(e);
}}
onSelect={track}
/>
<span className="field-punch-card__col" aria-hidden="true">
<b>{String(Math.min(caret + 1, COLS)).padStart(2, '0')}</b>/{COLS}
</span>
</label>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldPunchCard(el) on the mounted element instead.
<div class="field-punch-card">
<label class="field-punch-card__wrap" for="field-punch-card">
<span class="field-punch-card__head" style="transform:translateX(100.6px)" aria-hidden="true"></span>
<span class="field-punch-card__card" aria-hidden="true">
<span class="field-punch-card__band" style="transform:translateX(100.6px)"></span>
<span class="field-punch-card__digits" style="top:37.8px">000000000000000000000000</span>
<span class="field-punch-card__digits" style="top:45.2px">111111111111111111111111</span>
<span class="field-punch-card__digits" style="top:52.6px">222222222222222222222222</span>
<span class="field-punch-card__digits" style="top:60px">333333333333333333333333</span>
<span class="field-punch-card__digits" style="top:67.4px">444444444444444444444444</span>
<span class="field-punch-card__digits" style="top:74.80000000000001px">555555555555555555555555</span>
<span class="field-punch-card__digits" style="top:82.2px">666666666666666666666666</span>
<span class="field-punch-card__digits" style="top:89.60000000000001px">777777777777777777777777</span>
<span class="field-punch-card__digits" style="top:97px">888888888888888888888888</span>
<span class="field-punch-card__digits" style="top:104.4px">999999999999999999999999</span>
<span>
<span class="field-punch-card__printed" style="left:16px">R</span>
<span>
<i class="field-punch-card__hole" style="left:18.6px;top:30.1px"></i>
<i class="field-punch-card__chad" style="left:18.6px;top:30.1px;--dx:-1px;--r:-133deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:18.6px;top:104.1px"></i>
<i class="field-punch-card__chad" style="left:18.6px;top:104.1px;--dx:2px;--r:37deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:25.4px">U</span>
<span>
<i class="field-punch-card__hole" style="left:28px;top:37.5px"></i>
<i class="field-punch-card__chad" style="left:28px;top:37.5px;--dx:0px;--r:-85deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:28px;top:67.1px"></i>
<i class="field-punch-card__chad" style="left:28px;top:67.1px;--dx:3px;--r:-17deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:34.8px">N</span>
<span>
<i class="field-punch-card__hole" style="left:37.4px;top:30.1px"></i>
<i class="field-punch-card__chad" style="left:37.4px;top:30.1px;--dx:4px;--r:-71deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:37.4px;top:74.5px"></i>
<i class="field-punch-card__chad" style="left:37.4px;top:74.5px;--dx:4px;--r:31deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:44.2px"> </span>
</span>
<span>
<span class="field-punch-card__printed" style="left:53.6px">M</span>
<span>
<i class="field-punch-card__hole" style="left:56.2px;top:30.1px"></i>
<i class="field-punch-card__chad" style="left:56.2px;top:30.1px;--dx:0px;--r:-9deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:56.2px;top:67.1px"></i>
<i class="field-punch-card__chad" style="left:56.2px;top:67.1px;--dx:-3px;--r:76deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:63px">O</span>
<span>
<i class="field-punch-card__hole" style="left:65.6px;top:30.1px"></i>
<i class="field-punch-card__chad" style="left:65.6px;top:30.1px;--dx:-2px;--r:22deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:65.6px;top:81.9px"></i>
<i class="field-punch-card__chad" style="left:65.6px;top:81.9px;--dx:1px;--r:141deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:72.4px">V</span>
<span>
<i class="field-punch-card__hole" style="left:75px;top:37.5px"></i>
<i class="field-punch-card__chad" style="left:75px;top:37.5px;--dx:-1px;--r:70deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:75px;top:74.5px"></i>
<i class="field-punch-card__chad" style="left:75px;top:74.5px;--dx:-4px;--r:-145deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:81.8px">I</span>
<span>
<i class="field-punch-card__hole" style="left:84.4px;top:22.7px"></i>
<i class="field-punch-card__chad" style="left:84.4px;top:22.7px;--dx:0px;--r:67deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:84.4px;top:104.1px"></i>
<i class="field-punch-card__chad" style="left:84.4px;top:104.1px;--dx:-3px;--r:-46deg"></i>
</span>
</span>
<span>
<span class="field-punch-card__printed" style="left:91.2px">N</span>
<span>
<i class="field-punch-card__hole" style="left:93.8px;top:30.1px"></i>
<i class="field-punch-card__chad" style="left:93.8px;top:30.1px;--dx:1px;--r:115deg"></i>
</span>
<span>
<i class="field-punch-card__hole" style="left:93.8px;top:74.5px"></i>
<i class="field-punch-card__chad" style="left:93.8px;top:74.5px;--dx:1px;--r:-83deg"></i>
</span>
</span>
</span>
</label>
<label class="field-punch-card__field">
<span class="field-punch-card__prompt" aria-hidden="true">></span>
<input id="field-punch-card" class="field-punch-card__input" maxLength="24" autoComplete="off" spellCheck="false" placeholder="Type a command" aria-label="Command" value="RUN MOVIN"/>
<span class="field-punch-card__col" aria-hidden="true">
<b>10</b>
/24
</span>
</label>
</div>.field-punch-card { --card: light-dark(#dbdde1, #e5e6e9); --ink: light-dark(#50535b, #232529); --print: light-dark(#373a40, #121417); display: grid; justify-items: center; gap: 10px; }
.field-punch-card__card { position: relative; display: block; width: 258px; height: 114px; background: var(--card); clip-path: polygon(11px 0, 100% 0, 100% 100%, 0 100%, 0 11px); filter: drop-shadow(0 6px 8px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.field-punch-card__wrap { position: relative; padding-top: 10px; cursor: text; }
/* The keypunch line: a real field, so the caret, selection and editing are the browser's own. */
.field-punch-card__field { display: flex; align-items: center; gap: 8px; box-sizing: border-box; width: 258px; height: 36px; padding: 0 12px; border-radius: 8px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)); cursor: text; transition: box-shadow 0.2s; }
.field-punch-card__field:has(.field-punch-card__input:focus) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2)); }
.field-punch-card__field:has(.field-punch-card__input:focus-visible) { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-punch-card__prompt { color: light-dark(#6d7179, #7d8087); font: 600 13px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
.field-punch-card__input { flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); caret-color: light-dark(#373a40, #f1f2f3); font: 600 14px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.06em; text-transform: uppercase; outline: none; }
.field-punch-card__input::placeholder { color: light-dark(#6d7179, #7d8087); text-transform: none; letter-spacing: 0; font-weight: 500; }
.field-punch-card__col { color: light-dark(#5f626a, #a7abb2); font: 500 11px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; white-space: nowrap; font-variant-numeric: tabular-nums; }
.field-punch-card__col b { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.field-punch-card__digits { position: absolute; left: 19px; color: var(--ink); font: 500 5.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 6.10px; white-space: nowrap; pointer-events: none; }
.field-punch-card__band { position: absolute; left: 0; top: 0; bottom: 0; width: 9.4px; background: color-mix(in oklab, var(--print) 7%, transparent); transition: transform 0.25s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-punch-card__printed { position: absolute; top: 7px; width: 9.4px; color: var(--print); font: 700 7.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; animation: field-punch-card-print 0.3s ease-out 0.12s both; }
@keyframes field-punch-card-print { from { opacity: 0; transform: translateY(-3px); } }
/* A hole shows whatever is behind the card; its chad drops out of it. */
.field-punch-card__hole { position: absolute; width: 4.2px; height: 6px; border-radius: 0.8px; background: light-dark(#fbfcfd, #121417); box-shadow: inset 0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); animation: field-punch-card-punch 0.18s cubic-bezier(0.5, 0, 0.5, 1.6) both; }
@keyframes field-punch-card-punch { from { transform: scale(0.2); } }
.field-punch-card__chad { position: absolute; z-index: 2; width: 4.2px; height: 6px; border-radius: 0.8px; background: var(--card); box-shadow: 0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); opacity: 0; pointer-events: none; animation: field-punch-card-chad 0.8s cubic-bezier(0.4, 0, 0.9, 0.6) forwards; }
@keyframes field-punch-card-chad { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translate(var(--dx), 70px) rotate(var(--r)); } }
/* The punch head rides above the card to the next column. */
.field-punch-card__head { position: absolute; left: 0; top: 0; width: 9px; height: 8px; border-radius: 2px 2px 0 0; background: light-dark(#5f626a, #a7abb2); transition: transform 0.25s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-punch-card__head::after { content: ''; position: absolute; left: 3px; bottom: -3px; width: 3px; height: 3px; background: light-dark(#5f626a, #a7abb2); }
.field-punch-card:has(.field-punch-card__input:focus) .field-punch-card__head { background: light-dark(#373a40, #f1f2f3); }
.field-punch-card:has(.field-punch-card__input:focus) .field-punch-card__head::after { background: light-dark(#373a40, #f1f2f3); }
.field-punch-card[data-bad='0'] { animation: field-punch-card-nope-a 0.3s; }
.field-punch-card[data-bad='1'] { animation: field-punch-card-nope-b 0.3s; }
@keyframes field-punch-card-nope-a { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@keyframes field-punch-card-nope-b { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@media (prefers-reduced-motion: reduce) {
.field-punch-card * { animation: none !important; transition: none !important; }
.field-punch-card__chad { display: none; }
}function initFieldPunchCard(root) {
var COLS = 24;
var X0 = 16;
var CW = 9.4;
var Y0 = 22;
var RH = 7.4;
var head = root.querySelector('.field-punch-card__head');
var card = root.querySelector('.field-punch-card__card');
var band = root.querySelector('.field-punch-card__band');
var input = root.querySelector('.field-punch-card__input');
var count = root.querySelector('.field-punch-card__col');
var text = input.value;
var bad = 0;
var columns = [];
function colX(c) {
return +(X0 + c * CW).toFixed(1);
}
// Rows top to bottom: the 12 and 11 zones, then 0 to 9.
function row(r) {
return r === 'z12' ? 0 : r === 'z11' ? 1 : r + 2;
}
// The real card code: digits punch their row; letters add a zone punch on top.
function holes(ch) {
if (ch === ' ') return [];
if (/[0-9]/.test(ch)) return [row(Number(ch))];
var c = ch.charCodeAt(0);
if (ch >= 'A' && ch <= 'I') return [row('z12'), row(c - 64)];
if (ch >= 'J' && ch <= 'R') return [row('z11'), row(c - 73)];
if (ch >= 'S' && ch <= 'Z') return [row(0), row(c - 81)];
var extra = { '-': [row('z11')], '&': [row('z12')], '/': [row(0), row(1)], '.': [row('z12'), row(3), row(8)] };
return extra[ch] || null;
}
function column(c, ch) {
var g = document.createElement('span');
var x = +(colX(c) + 2.6).toFixed(1);
var html = '<span class="field-punch-card__printed" style="left:' + colX(c) + 'px"></span>';
holes(ch).forEach(function (r) {
var y = +(Y0 + r * RH + 0.7).toFixed(1);
html += '<i class="field-punch-card__hole" style="left:' + x + 'px;top:' + y + 'px"></i>';
html += '<i class="field-punch-card__chad" style="left:' + x + 'px;top:' + y + 'px;--dx:' + (((c * 7 + r * 3) % 9) - 4) + 'px;--r:' + (((c * 31 + r * 17) % 300) - 150) + 'deg"></i>';
});
g.innerHTML = html;
g.firstChild.textContent = ch;
return { ch: ch, el: g };
}
// Only a column whose character changed is punched again.
function render() {
var up = text.toUpperCase();
for (var c = 0; c < Math.max(up.length, columns.length); c++) {
if (c >= up.length) {
columns[c].el.remove();
continue;
}
if (columns[c] && columns[c].ch === up[c]) continue;
var fresh = column(c, up[c]);
if (columns[c]) card.replaceChild(fresh.el, columns[c].el);
else card.appendChild(fresh.el);
columns[c] = fresh;
}
columns.length = up.length;
place();
}
// The punch head waits over the column the caret is in.
function place() {
var caret = input.selectionStart == null ? text.length : input.selectionStart;
var x = 'translateX(' + colX(Math.min(caret, COLS - 1)) + 'px)';
head.style.transform = x;
band.style.transform = x;
count.innerHTML = '<b>' + String(Math.min(caret + 1, COLS)).padStart(2, '0') + '</b>/' + COLS;
}
function onInput() {
// Characters the card has no code for are refused, with a rattle.
var refused = input.value.toUpperCase().split('').some(function (ch) {
return holes(ch) === null;
});
if (refused) {
var at = Math.max(0, input.selectionStart - (input.value.length - text.length));
input.value = text;
input.setSelectionRange(at, at);
root.setAttribute('data-bad', String(bad++ % 2));
return;
}
text = input.value;
render();
}
function onSelection() {
if (document.activeElement === input) place();
}
// Build the columns from the field's starting value.
[].slice.call(card.children).forEach(function (n) {
if (!n.className) n.remove();
});
render();
input.addEventListener('input', onInput);
document.addEventListener('selectionchange', onSelection);
return function () {
input.removeEventListener('input', onInput);
document.removeEventListener('selectionchange', onSelection);
};
}
document.querySelectorAll('.field-punch-card').forEach(initFieldPunchCard);Goal: Create a "Punch Card" input.
Visual behavior: A command typed onto a punch card: every character punches its real column code, the chads fall out, the letter prints along the top and the punch head waits over the column the caret is in.
Technique:
- A real text field under the card holds the command, so the caret, selection and editing are the browser's own. The card is drawn from its value, and the punch head follows selectionStart.
- Characters map to the real Hollerith code: a digit punches its row, a letter adds a 12, 11 or 0 zone punch, and a few symbols have their own patterns. Anything without a code is refused and the card rattles.
- Each hole scales open with an overshoot while its chad, a copy in the card's colour, drops and spins away, and the printed letter fades in above its column.
- The punch head and a faint column band ride a transform to the caret's column on a springy cubic-bezier.
Accessibility: A native text input named "Command" with a visible placeholder; focus outlines the field, and clicking the card focuses it too. The card, the head and the column count are aria-hidden. Under prefers-reduced-motion holes appear without the punch or the falling chads.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Punch Card" input.
Visual behavior: A command typed onto a punch card: every character punches its real column code, the chads fall out, the letter prints along the top and the punch head waits over the column the caret is in.
Technique:
- A real text field under the card holds the command, so the caret, selection and editing are the browser's own. The card is drawn from its value, and the punch head follows selectionStart.
- Characters map to the real Hollerith code: a digit punches its row, a letter adds a 12, 11 or 0 zone punch, and a few symbols have their own patterns. Anything without a code is refused and the card rattles.
- Each hole scales open with an overshoot while its chad, a copy in the card's colour, drops and spins away, and the printed letter fades in above its column.
- The punch head and a faint column band ride a transform to the caret's column on a springy cubic-bezier.
Accessibility: A native text input named "Command" with a visible placeholder; focus outlines the field, and clicking the card focuses it too. The card, the head and the column count are aria-hidden. Under prefers-reduced-motion holes appear without the punch or the falling chads.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A command typed onto a punch card: every character punches its real column code, the chads fall out, the letter prints along the top and the punch head waits over the column the caret is in.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, focus, keyframe
- Techniques
- keyframes, state machine, css transition, text animation
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native text input named "Command" with a visible placeholder; focus outlines the field, and clicking the card focuses it too. The card, the head and the column count are aria-hidden. Under prefers-reduced-motion holes appear without the punch or the falling chads.