Monogram
import { useId, useState } from 'react';
const css = `
@keyframes field-monogram-in { from { transform: translateY(8px) scale(0.7); opacity: 0; filter: blur(3px); } to { transform: none; opacity: 1; filter: none; } }
.field-monogram { display: grid; justify-items: center; gap: 14px; width: 250px; }
.field-monogram__crest { position: relative; display: grid; place-items: center; width: 92px; height: 92px; border-radius: 50%; box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 0 0 5px light-dark(#fbfcfd, #121417), inset 0 0 0 6px light-dark(#cbced3, #393c41); }
.field-monogram__crest::before { content: ''; position: absolute; inset: 14px; border-radius: 50%; border: 1px dashed color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent); }
.field-monogram__pair { position: relative; width: 60px; height: 60px; font-family: Georgia, 'Times New Roman', serif; }
/* The two initials overlap on a diagonal, the second one set lighter so they interlock. */
.field-monogram__a, .field-monogram__b { position: absolute; line-height: 1; animation: field-monogram-in 0.5s cubic-bezier(0.3, 1.3, 0.5, 1) both; }
.field-monogram__a { left: 4px; top: 2px; color: light-dark(#373a40, #f1f2f3); font-size: 44px; }
.field-monogram__b { right: 2px; bottom: 0; color: light-dark(#50535b, #c6c8cd); font-size: 38px; font-style: italic; animation-delay: 0.08s; }
.field-monogram__empty { color: light-dark(#6d7179, #7d8087); font-family: Georgia, 'Times New Roman', serif; font-size: 13px; font-style: italic; }
.field-monogram__field { width: 100%; }
.field-monogram__label { display: block; margin-bottom: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12px; text-align: center; }
.field-monogram__input { width: 100%; height: 40px; padding: 0 12px; border: 0; border-radius: 10px; outline: 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font: inherit; font-size: 15px; text-align: center; transition: box-shadow 0.2s; }
.field-monogram__input:focus { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), 0 0 0 3px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); }
.field-monogram__input::placeholder { color: light-dark(#6d7179, #7d8087); }
@media (prefers-reduced-motion: reduce) {
.field-monogram__a, .field-monogram__b { animation: none; }
}
`;
export function MonogramInput() {
const id = useId();
const [name, setName] = useState('');
const words = name.trim().split(/\s+/).filter((w) => /\p{L}/u.test(w));
const first = words[0]?.match(/\p{L}/u)?.[0]?.toUpperCase() ?? '';
const last = words.length > 1 ? (words[words.length - 1]!.match(/\p{L}/u)?.[0]?.toUpperCase() ?? '') : '';
return (
<>
<style>
{css}
</style>
<div className="field-monogram">
<span className="field-monogram__crest" aria-hidden="true">
{first ? (
<span className="field-monogram__pair">
<span key={`a${first}`} className="field-monogram__a">
{first}
</span>
{last && (
<span key={`b${last}`} className="field-monogram__b">
{last}
</span>
)}
</span>
) : (
<span className="field-monogram__empty">initials</span>
)}
</span>
<div className="field-monogram__field">
<label className="field-monogram__label" htmlFor={id}>
Name for the engraving
</label>
<input
id={id}
className="field-monogram__input"
value={name}
maxLength={40}
autoComplete="name"
placeholder="First and last name"
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldMonogram(el) on the mounted element instead.
<div class="field-monogram">
<span class="field-monogram__crest" aria-hidden="true">
<span class="field-monogram__empty">initials</span>
</span>
<div class="field-monogram__field">
<label class="field-monogram__label" for="field-monogram">Name for the engraving</label>
<input id="field-monogram" class="field-monogram__input" maxLength="40" autoComplete="name" placeholder="First and last name" value=""/>
</div>
</div>@keyframes field-monogram-in { from { transform: translateY(8px) scale(0.7); opacity: 0; filter: blur(3px); } to { transform: none; opacity: 1; filter: none; } }
.field-monogram { display: grid; justify-items: center; gap: 14px; width: 250px; }
.field-monogram__crest { position: relative; display: grid; place-items: center; width: 92px; height: 92px; border-radius: 50%; box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 0 0 5px light-dark(#fbfcfd, #121417), inset 0 0 0 6px light-dark(#cbced3, #393c41); }
.field-monogram__crest::before { content: ''; position: absolute; inset: 14px; border-radius: 50%; border: 1px dashed color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent); }
.field-monogram__pair { position: relative; width: 60px; height: 60px; font-family: Georgia, 'Times New Roman', serif; }
/* The two initials overlap on a diagonal, the second one set lighter so they interlock. */
.field-monogram__a, .field-monogram__b { position: absolute; line-height: 1; animation: field-monogram-in 0.5s cubic-bezier(0.3, 1.3, 0.5, 1) both; }
.field-monogram__a { left: 4px; top: 2px; color: light-dark(#373a40, #f1f2f3); font-size: 44px; }
.field-monogram__b { right: 2px; bottom: 0; color: light-dark(#50535b, #c6c8cd); font-size: 38px; font-style: italic; animation-delay: 0.08s; }
.field-monogram__empty { color: light-dark(#6d7179, #7d8087); font-family: Georgia, 'Times New Roman', serif; font-size: 13px; font-style: italic; }
.field-monogram__field { width: 100%; }
.field-monogram__label { display: block; margin-bottom: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12px; text-align: center; }
.field-monogram__input { width: 100%; height: 40px; padding: 0 12px; border: 0; border-radius: 10px; outline: 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font: inherit; font-size: 15px; text-align: center; transition: box-shadow 0.2s; }
.field-monogram__input:focus { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), 0 0 0 3px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); }
.field-monogram__input::placeholder { color: light-dark(#6d7179, #7d8087); }
@media (prefers-reduced-motion: reduce) {
.field-monogram__a, .field-monogram__b { animation: none; }
}function initFieldMonogram(root) {
var input = root.querySelector('.field-monogram__input');
var crest = root.querySelector('.field-monogram__crest');
var shown = { first: '', last: '' };
function initial(w) {
var m = w && w.match(/\p{L}/u);
return m ? m[0].toUpperCase() : '';
}
function letter(cls, c) {
var el = document.createElement('span');
el.className = cls;
el.textContent = c;
return el;
}
function update() {
var words = input.value.trim().split(/\s+/).filter(function (w) { return /\p{L}/u.test(w); });
var first = initial(words[0]);
var last = words.length > 1 ? initial(words[words.length - 1]) : '';
if (first === shown.first && last === shown.last) return;
// Only a letter that changed is set again, so only it plays its entrance.
if (!first) {
crest.innerHTML = '<span class="field-monogram__empty">initials</span>';
} else {
var pair = crest.querySelector('.field-monogram__pair');
if (!pair || first !== shown.first) {
pair = document.createElement('span');
pair.className = 'field-monogram__pair';
pair.appendChild(letter('field-monogram__a', first));
crest.replaceChildren(pair);
shown.last = '';
}
if (last !== shown.last) {
var b = pair.querySelector('.field-monogram__b');
if (b) b.remove();
if (last) pair.appendChild(letter('field-monogram__b', last));
}
}
shown = { first: first, last: last };
}
input.addEventListener('input', update);
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.field-monogram').forEach(initFieldMonogram);Goal: Create a "Monogram" input.
Visual behavior: An engraving name field: as you type your name, your initials set themselves into an interlocked monogram in the crest above, the second letter in italic, each rising out of a blur.
Technique:
- The first letter of the first and last words become the two initials; they overlap on a diagonal, the second smaller, lighter and italic so they interlock.
- Each initial enters with a keyframe from below, small and blurred; only a letter that changed is replaced, so only it plays its entrance.
Accessibility: A real <input> with a visible <label>; decoration is aria-hidden. Under prefers-reduced-motion the initials appear without rising.
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 "Monogram" input.
Visual behavior: An engraving name field: as you type your name, your initials set themselves into an interlocked monogram in the crest above, the second letter in italic, each rising out of a blur.
Technique:
- The first letter of the first and last words become the two initials; they overlap on a diagonal, the second smaller, lighter and italic so they interlock.
- Each initial enters with a keyframe from below, small and blurred; only a letter that changed is replaced, so only it plays its entrance.
Accessibility: A real <input> with a visible <label>; decoration is aria-hidden. Under prefers-reduced-motion the initials appear without rising.
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
An engraving name field: as you type your name, your initials set themselves into an interlocked monogram in the crest above, the second letter in italic, each rising out of a blur.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- focus, state change, morph
- Techniques
- keyframes, filter, text animation
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <input> with a visible <label>; decoration is aria-hidden. Under prefers-reduced-motion the initials appear without rising.