Name tag
import { useId, useState } from 'react';
const css = `
@keyframes field-nametag-write { from { clip-path: inset(-20% 100% -20% -20%); } to { clip-path: inset(-20% -20% -20% -20%); } }
@keyframes field-nametag-slap-a { 0% { transform: translateY(-3px) rotate(-1deg) scale(1.02); } 45% { transform: rotate(-2deg) scale(0.97); } 100% { transform: rotate(-2deg); } }
@keyframes field-nametag-slap-b { 0% { transform: translateY(-3px) rotate(-1deg) scale(1.02); } 45% { transform: rotate(-2deg) scale(0.97); } 100% { transform: rotate(-2deg); } }
.field-nametag { width: 270px; padding: 6px; }
/* A white sticker in both themes; the band and marker ink stay dark on it. */
/* The shadow is a drop-shadow on this wrapper so it follows the curled corner. */
.field-nametag__lift {
border-radius: 14px;
filter: drop-shadow(0 1px 1px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25))) drop-shadow(0 3px 5px light-dark(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.12)));
transform: rotate(-2deg);
transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1), filter 0.35s;
}
.field-nametag__sticker {
position: relative;
overflow: hidden;
border-radius: 14px;
background: light-dark(#fbfcfd, #e5e6e9);
clip-path: polygon(0 0, 100% 0, 100% 100%, 100% 100%, 0 100%);
transition: clip-path 0.3s;
}
.field-nametag__band { display: block; padding: 10px 0 7px; background: light-dark(#42454c, #393c41); color: light-dark(#fbfcfd, #f1f2f3); text-align: center; cursor: text; }
.field-nametag__hello { display: block; font: 800 24px/1 system-ui, sans-serif; letter-spacing: 0.14em; }
.field-nametag__sub { display: block; margin-top: 3px; font-size: 12px; letter-spacing: 0.04em; }
.field-nametag__write { position: relative; height: 70px; }
.field-nametag__input, .field-nametag__mirror {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0 16px;
font: 400 32px/70px 'Marker Felt', 'Chalkboard SE', 'Comic Sans MS', 'Segoe Print', cursive;
font-kerning: none;
font-variant-ligatures: none;
text-align: center;
}
.field-nametag__input { position: relative; display: block; margin: 0; border: 0; outline: none; background: transparent; color: transparent; caret-color: light-dark(#373a40, #232529); }
.field-nametag__input::placeholder { color: light-dark(#cbced3, #7d8087); }
.field-nametag__input::selection { background: light-dark(#dbdde1, #a7abb2); }
.field-nametag__mirror { position: absolute; inset: 0; overflow: hidden; color: light-dark(#373a40, #232529); white-space: pre; pointer-events: none; }
.field-nametag__ch { display: inline-block; }
.field-nametag__ch--new { animation: field-nametag-write 0.24s ease-out; }
.field-nametag__foot { height: 10px; background: light-dark(#42454c, #393c41); }
/* While you write, the sticker lifts and its corner curls; it presses flat once you leave it. */
.field-nametag__curl { position: absolute; right: 0; bottom: 0; width: 0; height: 0; background: linear-gradient(135deg, light-dark(#cbced3, #a7abb2), light-dark(#ebedf0, #393c41)); clip-path: polygon(0 0, 100% 0, 0 100%); transition: width 0.3s, height 0.3s; }
.field-nametag__lift:focus-within { transform: translateY(-3px) rotate(-1deg); filter: drop-shadow(0 2px 2px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2))) drop-shadow(0 10px 10px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2))); outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.field-nametag__lift:focus-within .field-nametag__sticker { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 22px), calc(100% - 22px) 100%, 0 100%); }
.field-nametag__lift:focus-within .field-nametag__curl { width: 22px; height: 22px; }
.field-nametag__lift[data-slap='a'] { animation: field-nametag-slap-a 0.4s ease-out; }
.field-nametag__lift[data-slap='b'] { animation: field-nametag-slap-b 0.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
.field-nametag__lift, .field-nametag__sticker, .field-nametag__curl { transition: none; }
.field-nametag__ch--new, .field-nametag__lift[data-slap] { animation: none; }
}
`;
export function NametagInput() {
const id = useId();
const [v, setV] = useState('');
const [grew, setGrew] = useState(false);
const [slap, setSlap] = useState<'a' | 'b' | undefined>();
return (
<>
<style>
{css}
</style>
<div className="field-nametag">
<div className="field-nametag__lift" data-slap={slap}>
<div className="field-nametag__sticker">
<label className="field-nametag__band" htmlFor={id}>
<span className="field-nametag__hello">HELLO</span>
<span className="field-nametag__sub">my name is</span>
</label>
<div className="field-nametag__write">
<span className="field-nametag__mirror" aria-hidden="true">
{[...v].map((ch, i) => (
<span key={`${i}${ch}`} className={`field-nametag__ch${grew && i === v.length - 1 ? ' field-nametag__ch--new' : ''}`}>
{ch}
</span>
))}
</span>
<input
id={id}
className="field-nametag__input"
value={v}
maxLength={14}
placeholder="Your name"
autoComplete="given-name"
spellCheck={false}
onChange={(e) => {
setGrew(e.target.value.length > v.length);
setV(e.target.value);
}}
onBlur={() => v.trim() && setSlap((s) => (s === 'a' ? 'b' : 'a'))}
/>
</div>
<div className="field-nametag__foot" />
<span className="field-nametag__curl" aria-hidden="true" />
</div>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldNametag(el) on the mounted element instead.
<div class="field-nametag">
<div class="field-nametag__lift">
<div class="field-nametag__sticker">
<label class="field-nametag__band" for="field-nametag">
<span class="field-nametag__hello">HELLO</span>
<span class="field-nametag__sub">my name is</span>
</label>
<div class="field-nametag__write">
<span class="field-nametag__mirror" aria-hidden="true"></span>
<input id="field-nametag" class="field-nametag__input" maxLength="14" placeholder="Your name" autoComplete="given-name" spellCheck="false" value=""/>
</div>
<div class="field-nametag__foot"></div>
<span class="field-nametag__curl" aria-hidden="true"></span>
</div>
</div>
</div>@keyframes field-nametag-write { from { clip-path: inset(-20% 100% -20% -20%); } to { clip-path: inset(-20% -20% -20% -20%); } }
@keyframes field-nametag-slap-a { 0% { transform: translateY(-3px) rotate(-1deg) scale(1.02); } 45% { transform: rotate(-2deg) scale(0.97); } 100% { transform: rotate(-2deg); } }
@keyframes field-nametag-slap-b { 0% { transform: translateY(-3px) rotate(-1deg) scale(1.02); } 45% { transform: rotate(-2deg) scale(0.97); } 100% { transform: rotate(-2deg); } }
.field-nametag { width: 270px; padding: 6px; }
/* A white sticker in both themes; the band and marker ink stay dark on it. */
/* The shadow is a drop-shadow on this wrapper so it follows the curled corner. */
.field-nametag__lift {
border-radius: 14px;
filter: drop-shadow(0 1px 1px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25))) drop-shadow(0 3px 5px light-dark(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.12)));
transform: rotate(-2deg);
transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1), filter 0.35s;
}
.field-nametag__sticker {
position: relative;
overflow: hidden;
border-radius: 14px;
background: light-dark(#fbfcfd, #e5e6e9);
clip-path: polygon(0 0, 100% 0, 100% 100%, 100% 100%, 0 100%);
transition: clip-path 0.3s;
}
.field-nametag__band { display: block; padding: 10px 0 7px; background: light-dark(#42454c, #393c41); color: light-dark(#fbfcfd, #f1f2f3); text-align: center; cursor: text; }
.field-nametag__hello { display: block; font: 800 24px/1 system-ui, sans-serif; letter-spacing: 0.14em; }
.field-nametag__sub { display: block; margin-top: 3px; font-size: 12px; letter-spacing: 0.04em; }
.field-nametag__write { position: relative; height: 70px; }
.field-nametag__input, .field-nametag__mirror {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0 16px;
font: 400 32px/70px 'Marker Felt', 'Chalkboard SE', 'Comic Sans MS', 'Segoe Print', cursive;
font-kerning: none;
font-variant-ligatures: none;
text-align: center;
}
.field-nametag__input { position: relative; display: block; margin: 0; border: 0; outline: none; background: transparent; color: transparent; caret-color: light-dark(#373a40, #232529); }
.field-nametag__input::placeholder { color: light-dark(#cbced3, #7d8087); }
.field-nametag__input::selection { background: light-dark(#dbdde1, #a7abb2); }
.field-nametag__mirror { position: absolute; inset: 0; overflow: hidden; color: light-dark(#373a40, #232529); white-space: pre; pointer-events: none; }
.field-nametag__ch { display: inline-block; }
.field-nametag__ch--new { animation: field-nametag-write 0.24s ease-out; }
.field-nametag__foot { height: 10px; background: light-dark(#42454c, #393c41); }
/* While you write, the sticker lifts and its corner curls; it presses flat once you leave it. */
.field-nametag__curl { position: absolute; right: 0; bottom: 0; width: 0; height: 0; background: linear-gradient(135deg, light-dark(#cbced3, #a7abb2), light-dark(#ebedf0, #393c41)); clip-path: polygon(0 0, 100% 0, 0 100%); transition: width 0.3s, height 0.3s; }
.field-nametag__lift:focus-within { transform: translateY(-3px) rotate(-1deg); filter: drop-shadow(0 2px 2px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2))) drop-shadow(0 10px 10px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.2))); outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.field-nametag__lift:focus-within .field-nametag__sticker { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 22px), calc(100% - 22px) 100%, 0 100%); }
.field-nametag__lift:focus-within .field-nametag__curl { width: 22px; height: 22px; }
.field-nametag__lift[data-slap='a'] { animation: field-nametag-slap-a 0.4s ease-out; }
.field-nametag__lift[data-slap='b'] { animation: field-nametag-slap-b 0.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
.field-nametag__lift, .field-nametag__sticker, .field-nametag__curl { transition: none; }
.field-nametag__ch--new, .field-nametag__lift[data-slap] { animation: none; }
}function initFieldNametag(root) {
var input = root.querySelector('.field-nametag__input');
var mirror = root.querySelector('.field-nametag__mirror');
var lift = root.querySelector('.field-nametag__lift');
var v = '';
var slap = '';
// One span per letter; the newest one is written on with a left-to-right wipe.
function onInput() {
var next = input.value;
var grew = next.length > v.length;
v = next;
for (var i = 0; i < v.length; i++) {
var span = mirror.children[i];
if (span && span.textContent === v[i]) {
span.classList.remove('field-nametag__ch--new');
continue;
}
var fresh = document.createElement('span');
fresh.className = 'field-nametag__ch';
fresh.textContent = v[i];
if (span) mirror.replaceChild(fresh, span);
else mirror.appendChild(fresh);
}
while (mirror.children.length > v.length) mirror.lastChild.remove();
if (grew && mirror.lastChild) mirror.lastChild.classList.add('field-nametag__ch--new');
}
// Leaving a written name presses the sticker flat with a small slap.
function onBlur() {
if (!v.trim()) return;
slap = slap === 'a' ? 'b' : 'a';
lift.setAttribute('data-slap', slap);
}
input.addEventListener('input', onInput);
input.addEventListener('blur', onBlur);
return function () {
input.removeEventListener('input', onInput);
input.removeEventListener('blur', onBlur);
};
}
document.querySelectorAll('.field-nametag').forEach(initFieldNametag);Goal: Create a "Name tag" input.
Visual behavior: A "Hello, my name is" sticker: your name is written on in marker one letter at a time, and the sticker lifts at the corner while you type and presses flat when you are done.
Technique:
- A real input with transparent text sits under a mirror of the value set in the same font and metrics, so the caret, selection and typing all still work while the mirror draws the letters.
- The newest letter wipes in with an animated clip-path inset, like a pen stroke. Focus lifts and tilts the sticker and cuts its corner with a clip-path polygon over a curled flap; the shadow is a drop-shadow filter on a wrapper, so it follows the curl.
- Leaving a written name plays a slap keyframe that presses the sticker flat.
Accessibility: The "Hello, my name is" band is the input's <label>, and the mirror is aria-hidden. Focus outlines the sticker. Under prefers-reduced-motion it lifts and writes without motion.
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 "Name tag" input.
Visual behavior: A "Hello, my name is" sticker: your name is written on in marker one letter at a time, and the sticker lifts at the corner while you type and presses flat when you are done.
Technique:
- A real input with transparent text sits under a mirror of the value set in the same font and metrics, so the caret, selection and typing all still work while the mirror draws the letters.
- The newest letter wipes in with an animated clip-path inset, like a pen stroke. Focus lifts and tilts the sticker and cuts its corner with a clip-path polygon over a curled flap; the shadow is a drop-shadow filter on a wrapper, so it follows the curl.
- Leaving a written name plays a slap keyframe that presses the sticker flat.
Accessibility: The "Hello, my name is" band is the input's <label>, and the mirror is aria-hidden. Focus outlines the sticker. Under prefers-reduced-motion it lifts and writes without motion.
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 "Hello, my name is" sticker: your name is written on in marker one letter at a time, and the sticker lifts at the corner while you type and presses flat when you are done.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, focus, keyframe
- Techniques
- keyframes, clip path, filter
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The "Hello, my name is" band is the input's <label>, and the mirror is aria-hidden. Focus outlines the sticker. Under prefers-reduced-motion it lifts and writes without motion.