Jersey
import { useId, useState, type CSSProperties } from 'react';
const STEP = 6.4; // degrees between letters along the arch
const SHIRT = 'M84 6Q131 26 178 6L222 24L246 64L214 80L204 66V160Q131 168 58 160V66L48 80L16 64L40 24Z';
const css = `
.field-jersey { --shirt: light-dark(#42454c, #232529); --print: light-dark(#fbfcfd, #f1f2f3); --trim: color-mix(in oklab, var(--shirt) 70%, var(--print)); display: grid; justify-items: center; gap: 6px; width: 262px; }
.field-jersey__shirt { position: relative; width: 262px; height: 166px; }
.field-jersey__art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 10px 12px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.field-jersey__cloth { fill: var(--shirt); }
.field-jersey__mesh { fill: url(#field-jersey-mesh); }
.field-jersey__mesh-dot { fill: color-mix(in oklab, var(--print) 9%, transparent); }
.field-jersey__trim { fill: none; stroke: var(--trim); stroke-width: 3; stroke-linecap: round; }
/* Name letters sit on an arch: each rotates about a point far below the shoulders. */
.field-jersey__letter { position: absolute; left: 131px; top: 30px; width: 20px; margin-left: -10px; color: var(--print); font: 900 17px/1 'Arial Black', Impact, system-ui, sans-serif; text-align: center; transform-origin: 50% 200px; transform: rotate(var(--a)); transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1); }
.field-jersey__letter > b { display: block; font-weight: 900; animation: field-jersey-press 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
.field-jersey__num { position: absolute; left: 0; right: 0; top: 54px; color: var(--print); font: 900 66px/1 'Arial Black', Impact, system-ui, sans-serif; letter-spacing: -0.02em; text-align: center; -webkit-text-stroke: 2px var(--trim); paint-order: stroke fill; animation: field-jersey-press 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
@keyframes field-jersey-press { 0% { opacity: 0; transform: scale(1.5); filter: blur(2px); } 55% { opacity: 1; transform: scale(0.94); filter: blur(0); } 100% { transform: none; } }
/* Steam off the press. */
.field-jersey__steam { position: absolute; left: 50%; top: 0; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 40%, transparent); filter: blur(2px); opacity: 0; animation: field-jersey-steam 0.9s ease-out 0.15s; pointer-events: none; }
@keyframes field-jersey-steam { 0% { opacity: 0.8; transform: translate(0, 0) scale(0.6); } 100% { opacity: 0; transform: translate(var(--dx), -24px) scale(1.8); } }
.field-jersey__fields { display: flex; gap: 8px; }
.field-jersey__field { display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 10px; border-radius: 9px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#6d7179, #7d8087); font: 700 9px system-ui, sans-serif; letter-spacing: 0.1em; text-transform: uppercase; }
.field-jersey__field:has(input:focus-visible) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), 0 0 0 2px light-dark(#5f626a, #a7abb2); }
.field-jersey__input { width: 100px; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); font: 800 14px system-ui, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; outline: none; }
.field-jersey__input--num { width: 26px; }
@media (prefers-reduced-motion: reduce) {
.field-jersey * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const id = useId();
const [name, setName] = useState('RIVERA');
const [num, setNum] = useState('10');
const letters = [...name];
return (
<>
<style href="kit-inputs-jersey" precedence="kit">
{css}
</style>
<div className="field-jersey">
<div className="field-jersey__shirt" aria-hidden="true">
<svg className="field-jersey__art" viewBox="0 0 262 166">
<defs>
<pattern id="field-jersey-mesh" width={4} height={4} patternUnits="userSpaceOnUse">
<circle className="field-jersey__mesh-dot" cx={2} cy={2} r={0.9} />
</pattern>
</defs>
<path className="field-jersey__cloth" d={SHIRT} />
<path className="field-jersey__mesh" d={SHIRT} />
<path className="field-jersey__trim" d="M84 7Q131 27 178 7M50 78L16 63M212 78L246 63" />
</svg>
{letters.map((ch, i) => (
<span key={`${i}${ch}`} className="field-jersey__letter" style={{ '--a': `${(i - (letters.length - 1) / 2) * STEP}deg` } as CSSProperties}>
<b>{ch}</b>
{[-6, 0, 6].map((dx) => (
<i key={dx} className="field-jersey__steam" style={{ '--dx': `${dx}px` } as CSSProperties} />
))}
</span>
))}
<span key={num} className="field-jersey__num">
{num}
</span>
</div>
<div className="field-jersey__fields">
<label className="field-jersey__field">
Name
<input id={id} className="field-jersey__input" maxLength={10} autoComplete="family-name" value={name} onChange={(e) => setName(e.target.value.toUpperCase().replace(/[^A-Z' .-]/g, ''))} />
</label>
<label className="field-jersey__field">
No.
<input className="field-jersey__input field-jersey__input--num" inputMode="numeric" maxLength={2} value={num} onChange={(e) => setNum(e.target.value.replace(/\D/g, '').slice(0, 2))} />
</label>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldJersey(el) on the mounted element instead.
<div class="field-jersey">
<div class="field-jersey__shirt" aria-hidden="true">
<svg class="field-jersey__art" viewBox="0 0 262 166">
<defs>
<pattern id="field-jersey-mesh" width="4" height="4" patternUnits="userSpaceOnUse">
<circle class="field-jersey__mesh-dot" cx="2" cy="2" r="0.9"></circle>
</pattern>
</defs>
<path class="field-jersey__cloth" d="M84 6Q131 26 178 6L222 24L246 64L214 80L204 66V160Q131 168 58 160V66L48 80L16 64L40 24Z"></path>
<path class="field-jersey__mesh" d="M84 6Q131 26 178 6L222 24L246 64L214 80L204 66V160Q131 168 58 160V66L48 80L16 64L40 24Z"></path>
<path class="field-jersey__trim" d="M84 7Q131 27 178 7M50 78L16 63M212 78L246 63"></path>
</svg>
<span class="field-jersey__letter" style="--a:-16deg">
<b>R</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__letter" style="--a:-9.600000000000001deg">
<b>I</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__letter" style="--a:-3.2deg">
<b>V</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__letter" style="--a:3.2deg">
<b>E</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__letter" style="--a:9.600000000000001deg">
<b>R</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__letter" style="--a:16deg">
<b>A</b>
<i class="field-jersey__steam" style="--dx:-6px"></i>
<i class="field-jersey__steam" style="--dx:0px"></i>
<i class="field-jersey__steam" style="--dx:6px"></i>
</span>
<span class="field-jersey__num">10</span>
</div>
<div class="field-jersey__fields">
<label class="field-jersey__field">
Name
<input id="field-jersey" class="field-jersey__input" maxLength="10" autoComplete="family-name" value="RIVERA"/>
</label>
<label class="field-jersey__field">
No.
<input class="field-jersey__input field-jersey__input--num" inputMode="numeric" maxLength="2" value="10"/>
</label>
</div>
</div>.field-jersey { --shirt: light-dark(#42454c, #232529); --print: light-dark(#fbfcfd, #f1f2f3); --trim: color-mix(in oklab, var(--shirt) 70%, var(--print)); display: grid; justify-items: center; gap: 6px; width: 262px; }
.field-jersey__shirt { position: relative; width: 262px; height: 166px; }
.field-jersey__art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 10px 12px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.field-jersey__cloth { fill: var(--shirt); }
.field-jersey__mesh { fill: url(#field-jersey-mesh); }
.field-jersey__mesh-dot { fill: color-mix(in oklab, var(--print) 9%, transparent); }
.field-jersey__trim { fill: none; stroke: var(--trim); stroke-width: 3; stroke-linecap: round; }
/* Name letters sit on an arch: each rotates about a point far below the shoulders. */
.field-jersey__letter { position: absolute; left: 131px; top: 30px; width: 20px; margin-left: -10px; color: var(--print); font: 900 17px/1 'Arial Black', Impact, system-ui, sans-serif; text-align: center; transform-origin: 50% 200px; transform: rotate(var(--a)); transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1); }
.field-jersey__letter > b { display: block; font-weight: 900; animation: field-jersey-press 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
.field-jersey__num { position: absolute; left: 0; right: 0; top: 54px; color: var(--print); font: 900 66px/1 'Arial Black', Impact, system-ui, sans-serif; letter-spacing: -0.02em; text-align: center; -webkit-text-stroke: 2px var(--trim); paint-order: stroke fill; animation: field-jersey-press 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
@keyframes field-jersey-press { 0% { opacity: 0; transform: scale(1.5); filter: blur(2px); } 55% { opacity: 1; transform: scale(0.94); filter: blur(0); } 100% { transform: none; } }
/* Steam off the press. */
.field-jersey__steam { position: absolute; left: 50%; top: 0; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 40%, transparent); filter: blur(2px); opacity: 0; animation: field-jersey-steam 0.9s ease-out 0.15s; pointer-events: none; }
@keyframes field-jersey-steam { 0% { opacity: 0.8; transform: translate(0, 0) scale(0.6); } 100% { opacity: 0; transform: translate(var(--dx), -24px) scale(1.8); } }
.field-jersey__fields { display: flex; gap: 8px; }
.field-jersey__field { display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 10px; border-radius: 9px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#6d7179, #7d8087); font: 700 9px system-ui, sans-serif; letter-spacing: 0.1em; text-transform: uppercase; }
.field-jersey__field:has(input:focus-visible) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), 0 0 0 2px light-dark(#5f626a, #a7abb2); }
.field-jersey__input { width: 100px; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); font: 800 14px system-ui, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; outline: none; }
.field-jersey__input--num { width: 26px; }
@media (prefers-reduced-motion: reduce) {
.field-jersey * { animation: none !important; transition: none !important; }
}function initFieldJersey(root) {
var STEP = 6.4; // degrees between letters along the arch
var shirt = root.querySelector('.field-jersey__shirt');
var nameInput = root.querySelector('.field-jersey__input:not(.field-jersey__input--num)');
var numInput = root.querySelector('.field-jersey__input--num');
var num = root.querySelector('.field-jersey__num');
var letters = [].slice.call(shirt.querySelectorAll('.field-jersey__letter')).map(function (el) {
return { ch: el.querySelector('b').textContent, el: el };
});
// A letter is pressed on again only when it changes; the whole arch re-centres.
function letter(ch) {
var el = document.createElement('span');
el.className = 'field-jersey__letter';
el.innerHTML = '<b></b>' + [-6, 0, 6].map(function (dx) {
return '<i class="field-jersey__steam" style="--dx:' + dx + 'px"></i>';
}).join('');
el.firstChild.textContent = ch;
return el;
}
function onName() {
var name = nameInput.value.toUpperCase().replace(/[^A-Z' .-]/g, '');
nameInput.value = name;
for (var i = 0; i < Math.max(name.length, letters.length); i++) {
if (i >= name.length) {
letters[i].el.remove();
continue;
}
if (!letters[i] || letters[i].ch !== name[i]) {
var el = letter(name[i]);
if (letters[i]) letters[i].el.replaceWith(el);
else shirt.insertBefore(el, num);
letters[i] = { ch: name[i], el: el };
}
}
letters.length = name.length;
letters.forEach(function (l, i) {
l.el.style.setProperty('--a', (i - (name.length - 1) / 2) * STEP + 'deg');
});
}
function onNum() {
numInput.value = numInput.value.replace(/\D/g, '').slice(0, 2);
if (numInput.value === num.textContent) return;
// A new number presses on fresh.
var fresh = num.cloneNode(false);
fresh.textContent = numInput.value;
num.replaceWith(fresh);
num = fresh;
}
nameInput.addEventListener('input', onName);
numInput.addEventListener('input', onNum);
return function () {
nameInput.removeEventListener('input', onName);
numInput.removeEventListener('input', onNum);
};
}
document.querySelectorAll('.field-jersey').forEach(initFieldJersey);Goal: Create a "Jersey" input.
Visual behavior: A name and number on the back of a shirt: every letter is heat-pressed on in an arch across the shoulders with a puff of steam, and the number presses on below.
Technique:
- Two real inputs, a name and a two-digit number, each in its own labelled field; the shirt is drawn from their values.
- The name sits on an arch: every letter rotates about a point far below the shoulders, by a step angle times its distance from the middle, so the arch stays centred at any length.
- A new letter or number presses on from scale 1.5 and a blur, settles past its size, and puffs three blurred steam dots up and away.
- The shirt is one SVG path with a dotted mesh pattern and trim, under a soft drop shadow.
Accessibility: Two native inputs labelled by visible text, "Name" and "No."; focus outlines the field. The shirt drawing is aria-hidden. Under prefers-reduced-motion letters appear without the press or steam.
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 "Jersey" input.
Visual behavior: A name and number on the back of a shirt: every letter is heat-pressed on in an arch across the shoulders with a puff of steam, and the number presses on below.
Technique:
- Two real inputs, a name and a two-digit number, each in its own labelled field; the shirt is drawn from their values.
- The name sits on an arch: every letter rotates about a point far below the shoulders, by a step angle times its distance from the middle, so the arch stays centred at any length.
- A new letter or number presses on from scale 1.5 and a blur, settles past its size, and puffs three blurred steam dots up and away.
- The shirt is one SVG path with a dotted mesh pattern and trim, under a soft drop shadow.
Accessibility: Two native inputs labelled by visible text, "Name" and "No."; focus outlines the field. The shirt drawing is aria-hidden. Under prefers-reduced-motion letters appear without the press or steam.
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 name and number on the back of a shirt: every letter is heat-pressed on in an arch across the shoulders with a puff of steam, and the number presses on below.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, focus, keyframe
- Techniques
- keyframes, css variables, transform only, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Two native inputs labelled by visible text, "Name" and "No."; focus outlines the field. The shirt drawing is aria-hidden. Under prefers-reduced-motion letters appear without the press or steam.