Jersey

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/input-jersey
Source on GitHub

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.

More inputs

See all
Key
Notebook

7 of 10 digits

Pager
Plate