6080100120140160180200Leo · 3Mia · 7Ada · 11You · todayEnter to mark it
Freight
- motion
- ui kit
React + Tailwind
import { useId, useRef, useState, type CSSProperties } from 'react';
const MAX = 3;
const wagonWidth = (t: string) => Math.max(38, Math.min(64, 14 + t.length * 5.8));
const css = `
.field-freight { --steel: light-dark(#50535b, #7d8087); --body: light-dark(#6d7179, #393c41); --plank: color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)); --paint: light-dark(#42454c, #f1f2f3); --label: light-dark(#fbfcfd, #f1f2f3); display: grid; gap: 10px; width: 262px; }
.field-freight__yard { position: relative; height: 92px; overflow: hidden; }
.field-freight__rails { position: absolute; left: 0; right: 0; bottom: 6px; height: 7px; background: linear-gradient(var(--steel), var(--steel)) 0 0 / 100% 2px no-repeat, repeating-linear-gradient(90deg, color-mix(in oklab, var(--steel) 55%, transparent) 0 5px, transparent 5px 12px) 0 2px / 100% 5px no-repeat; }
.field-freight__train { position: absolute; left: 4px; bottom: 13px; display: flex; align-items: flex-end; }
.field-freight__train[data-jolt='0'] { animation: field-freight-jolt-a 0.45s ease-out 0.62s; }
.field-freight__train[data-jolt='1'] { animation: field-freight-jolt-b 0.45s ease-out 0.62s; }
@keyframes field-freight-jolt-a { 25% { transform: translateX(-3px); } 60% { transform: translateX(1px); } }
@keyframes field-freight-jolt-b { 25% { transform: translateX(-3px); } 60% { transform: translateX(1px); } }
/* The engine, facing left, with its chimney and three drivers. */
.field-freight__loco { position: relative; width: 60px; height: 46px; flex: none; }
.field-freight__loco i { position: absolute; display: block; background: var(--paint); }
.field-freight__wheel { position: absolute; bottom: -4px; width: 11px; height: 11px; border-radius: 50%; background: radial-gradient(circle, var(--steel) 0 2px, light-dark(#fbfcfd, #121417) 2.5px 3.5px, var(--steel) 4px); box-shadow: 0 0 0 1px light-dark(#fbfcfd, #121417); }
.field-freight__puff { position: absolute; left: 9px; top: -6px; width: 10px; height: 10px; border-radius: 50%; background: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 55%, transparent); opacity: 0; animation: field-freight-puff 1.2s ease-out forwards; }
@keyframes field-freight-puff { 0% { opacity: 0.9; transform: scale(0.4); } 100% { opacity: 0; transform: translate(var(--dx), -34px) scale(2.2); } }
.field-freight__link { width: 6px; height: 3px; margin-bottom: 7px; flex: none; background: var(--steel); }
/* A wagon: plank sides, a stencilled tag, rolled in from the right to couple on. */
.field-freight__wagon { position: relative; flex: none; height: 34px; border-radius: 2px; background: repeating-linear-gradient(90deg, var(--body) 0 6px, var(--plank) 6px 7px); box-shadow: inset 0 2px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), inset 0 -3px 0 var(--plank); animation: field-freight-roll 0.75s cubic-bezier(0.15, 0.6, 0.3, 1) both; }
@keyframes field-freight-roll { from { transform: translateX(240px); } }
.field-freight__wagon .field-freight__wheel { width: 9px; height: 9px; bottom: -5px; }
.field-freight__tag { position: absolute; left: 0; right: 0; top: 11px; color: var(--label); font: 400 9px/1 Stencil, 'Stencil Std', Impact, 'Arial Black', sans-serif; letter-spacing: 0.06em; text-align: center; text-transform: uppercase; white-space: nowrap; }
.field-freight__off { position: absolute; right: -4px; top: -6px; display: grid; place-items: center; width: 14px; height: 14px; padding: 0; border: 0; border-radius: 50%; background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); font: 600 10px/1 system-ui, sans-serif; opacity: 0; cursor: pointer; transition: opacity 0.2s; }
.field-freight__wagon:hover .field-freight__off, .field-freight__off:focus-visible { opacity: 1; }
.field-freight__off:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); }
.field-freight__gone { position: absolute; bottom: 13px; pointer-events: none; animation: field-freight-away 0.9s cubic-bezier(0.5, 0, 0.9, 0.6) forwards; }
@keyframes field-freight-away { to { transform: translateX(260px); } }
.field-freight__slip { display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 6px 0 12px; border-radius: 8px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.field-freight__slip:has(.field-freight__input:focus-visible) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), 0 0 0 2px light-dark(#5f626a, #a7abb2); }
.field-freight__slip[data-full='0'] { animation: field-freight-full-a 0.3s; }
.field-freight__slip[data-full='1'] { animation: field-freight-full-b 0.3s; }
@keyframes field-freight-full-a { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@keyframes field-freight-full-b { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
.field-freight__input { flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); font: 500 14px system-ui, sans-serif; outline: none; }
.field-freight__input::placeholder { color: light-dark(#6d7179, #7d8087); }
.field-freight__key { padding: 3px 6px; border-radius: 5px; background: light-dark(#ebedf0, #232529); color: light-dark(#5f626a, #a7abb2); font: 600 10px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
@media (prefers-reduced-motion: reduce) {
.field-freight * { animation: none !important; }
.field-freight__gone { display: none; }
}
`;
function Wheels({ at }: { at: number[] }) {
return (
<>
{at.map((l) => (
<span key={l} className="field-freight__wheel" style={{ left: l }} />
))}
</>
);
}
export function Preview() {
const id = useId();
const [tags, setTags] = useState(['motion', 'ui kit']);
const [draft, setDraft] = useState('');
const [adds, setAdds] = useState(0);
const [full, setFull] = useState(0);
const [gone, setGone] = useState<{ tag: string; x: number; n: number } | null>(null);
const n = useRef(0);
const add = () => {
const t = draft.trim().slice(0, 9);
if (!t) return;
if (tags.length >= MAX || tags.includes(t)) return setFull((f) => f + 1);
setTags([...tags, t]);
setDraft('');
setAdds((a) => a + 1);
};
const remove = (i: number) => {
// The uncoupled wagon rolls away from where it stood.
const x = 4 + 60 + tags.slice(0, i).reduce((s, t) => s + 6 + wagonWidth(t), 0) + 6;
setGone({ tag: tags[i], x, n: ++n.current });
setTags(tags.filter((_, k) => k !== i));
};
return (
<>
<style href="kit-inputs-freight" precedence="kit">
{css}
</style>
<div className="field-freight">
<div className="field-freight__yard">
<span className="field-freight__rails" aria-hidden="true" />
<div className="field-freight__train" data-jolt={adds ? adds % 2 : undefined}>
<span className="field-freight__loco" aria-hidden="true">
<i style={{ left: 6, top: 4, width: 7, height: 12, borderRadius: '2px 2px 0 0' }} />
<i style={{ left: 2, top: 14, width: 40, height: 20, borderRadius: '9px 2px 2px 9px' }} />
<i style={{ left: 38, top: 2, width: 20, height: 32, borderRadius: 2 }} />
<i style={{ left: 42, top: 7, width: 10, height: 8, background: 'light-dark(#fbfcfd, #121417)', borderRadius: 1 }} />
<i style={{ left: 0, top: 33, width: 58, height: 5, borderRadius: 1 }} />
<Wheels at={[6, 22, 42]} />
<span key={adds} className="field-freight__puff" style={{ '--dx': '-14px' } as CSSProperties} />
</span>
<ul style={{ display: 'flex', alignItems: 'flex-end', margin: 0, padding: 0, listStyle: 'none' }} aria-label="Tags">
{tags.map((t, i) => (
<li key={t} style={{ display: 'flex', alignItems: 'flex-end' }}>
<span className="field-freight__link" aria-hidden="true" />
<span className="field-freight__wagon" style={{ width: wagonWidth(t) }}>
<span className="field-freight__tag">{t}</span>
<Wheels at={[5, wagonWidth(t) - 14]} />
<button type="button" className="field-freight__off" aria-label={`Remove ${t}`} onClick={() => remove(i)}>
×
</button>
</span>
</li>
))}
</ul>
</div>
{gone && (
<span key={gone.n} className="field-freight__gone" style={{ left: gone.x }} aria-hidden="true" onAnimationEnd={() => setGone(null)}>
<span className="field-freight__wagon" style={{ display: 'block', width: wagonWidth(gone.tag), animation: 'none' }}>
<span className="field-freight__tag">{gone.tag}</span>
<Wheels at={[5, wagonWidth(gone.tag) - 14]} />
</span>
</span>
)}
</div>
<label className="field-freight__slip" data-full={full ? full % 2 : undefined}>
<input
id={id}
className="field-freight__input"
aria-label="Add a tag"
placeholder={tags.length >= MAX ? 'Train is full' : 'Add a tag'}
maxLength={9}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
add();
} else if (e.key === 'Backspace' && !draft && tags.length) remove(tags.length - 1);
}}
/>
<span className="field-freight__key" aria-hidden="true">
Enter ↵
</span>
</label>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldFreight(el) on the mounted element instead.
HTML
<div class="field-freight">
<div class="field-freight__yard">
<span class="field-freight__rails" aria-hidden="true"></span>
<div class="field-freight__train">
<span class="field-freight__loco" aria-hidden="true">
<i style="left:6px;top:4px;width:7px;height:12px;border-radius:2px 2px 0 0"></i>
<i style="left:2px;top:14px;width:40px;height:20px;border-radius:9px 2px 2px 9px"></i>
<i style="left:38px;top:2px;width:20px;height:32px;border-radius:2px"></i>
<i style="left:42px;top:7px;width:10px;height:8px;background:light-dark(#fbfcfd, #121417);border-radius:1px"></i>
<i style="left:0;top:33px;width:58px;height:5px;border-radius:1px"></i>
<span class="field-freight__wheel" style="left:6px"></span>
<span class="field-freight__wheel" style="left:22px"></span>
<span class="field-freight__wheel" style="left:42px"></span>
<span class="field-freight__puff" style="--dx:-14px"></span>
</span>
<ul style="display:flex;align-items:flex-end;margin:0;padding:0;list-style:none" aria-label="Tags">
<li style="display:flex;align-items:flex-end">
<span class="field-freight__link" aria-hidden="true"></span>
<span class="field-freight__wagon" style="width:48.8px">
<span class="field-freight__tag">motion</span>
<span class="field-freight__wheel" style="left:5px"></span>
<span class="field-freight__wheel" style="left:34.8px"></span>
<button type="button" class="field-freight__off" aria-label="Remove motion">×</button>
</span>
</li>
<li style="display:flex;align-items:flex-end">
<span class="field-freight__link" aria-hidden="true"></span>
<span class="field-freight__wagon" style="width:48.8px">
<span class="field-freight__tag">ui kit</span>
<span class="field-freight__wheel" style="left:5px"></span>
<span class="field-freight__wheel" style="left:34.8px"></span>
<button type="button" class="field-freight__off" aria-label="Remove ui kit">×</button>
</span>
</li>
</ul>
</div>
</div>
<label class="field-freight__slip">
<input id="field-freight" class="field-freight__input" aria-label="Add a tag" placeholder="Add a tag" maxLength="9" value=""/>
<span class="field-freight__key" aria-hidden="true">Enter ↵</span>
</label>
</div>CSS
.field-freight { --steel: light-dark(#50535b, #7d8087); --body: light-dark(#6d7179, #393c41); --plank: color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)); --paint: light-dark(#42454c, #f1f2f3); --label: light-dark(#fbfcfd, #f1f2f3); display: grid; gap: 10px; width: 262px; }
.field-freight__yard { position: relative; height: 92px; overflow: hidden; }
.field-freight__rails { position: absolute; left: 0; right: 0; bottom: 6px; height: 7px; background: linear-gradient(var(--steel), var(--steel)) 0 0 / 100% 2px no-repeat, repeating-linear-gradient(90deg, color-mix(in oklab, var(--steel) 55%, transparent) 0 5px, transparent 5px 12px) 0 2px / 100% 5px no-repeat; }
.field-freight__train { position: absolute; left: 4px; bottom: 13px; display: flex; align-items: flex-end; }
.field-freight__train[data-jolt='0'] { animation: field-freight-jolt-a 0.45s ease-out 0.62s; }
.field-freight__train[data-jolt='1'] { animation: field-freight-jolt-b 0.45s ease-out 0.62s; }
@keyframes field-freight-jolt-a { 25% { transform: translateX(-3px); } 60% { transform: translateX(1px); } }
@keyframes field-freight-jolt-b { 25% { transform: translateX(-3px); } 60% { transform: translateX(1px); } }
/* The engine, facing left, with its chimney and three drivers. */
.field-freight__loco { position: relative; width: 60px; height: 46px; flex: none; }
.field-freight__loco i { position: absolute; display: block; background: var(--paint); }
.field-freight__wheel { position: absolute; bottom: -4px; width: 11px; height: 11px; border-radius: 50%; background: radial-gradient(circle, var(--steel) 0 2px, light-dark(#fbfcfd, #121417) 2.5px 3.5px, var(--steel) 4px); box-shadow: 0 0 0 1px light-dark(#fbfcfd, #121417); }
.field-freight__puff { position: absolute; left: 9px; top: -6px; width: 10px; height: 10px; border-radius: 50%; background: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 55%, transparent); opacity: 0; animation: field-freight-puff 1.2s ease-out forwards; }
@keyframes field-freight-puff { 0% { opacity: 0.9; transform: scale(0.4); } 100% { opacity: 0; transform: translate(var(--dx), -34px) scale(2.2); } }
.field-freight__link { width: 6px; height: 3px; margin-bottom: 7px; flex: none; background: var(--steel); }
/* A wagon: plank sides, a stencilled tag, rolled in from the right to couple on. */
.field-freight__wagon { position: relative; flex: none; height: 34px; border-radius: 2px; background: repeating-linear-gradient(90deg, var(--body) 0 6px, var(--plank) 6px 7px); box-shadow: inset 0 2px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), inset 0 -3px 0 var(--plank); animation: field-freight-roll 0.75s cubic-bezier(0.15, 0.6, 0.3, 1) both; }
@keyframes field-freight-roll { from { transform: translateX(240px); } }
.field-freight__wagon .field-freight__wheel { width: 9px; height: 9px; bottom: -5px; }
.field-freight__tag { position: absolute; left: 0; right: 0; top: 11px; color: var(--label); font: 400 9px/1 Stencil, 'Stencil Std', Impact, 'Arial Black', sans-serif; letter-spacing: 0.06em; text-align: center; text-transform: uppercase; white-space: nowrap; }
.field-freight__off { position: absolute; right: -4px; top: -6px; display: grid; place-items: center; width: 14px; height: 14px; padding: 0; border: 0; border-radius: 50%; background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); font: 600 10px/1 system-ui, sans-serif; opacity: 0; cursor: pointer; transition: opacity 0.2s; }
.field-freight__wagon:hover .field-freight__off, .field-freight__off:focus-visible { opacity: 1; }
.field-freight__off:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); }
.field-freight__gone { position: absolute; bottom: 13px; pointer-events: none; animation: field-freight-away 0.9s cubic-bezier(0.5, 0, 0.9, 0.6) forwards; }
@keyframes field-freight-away { to { transform: translateX(260px); } }
.field-freight__slip { display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 6px 0 12px; border-radius: 8px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.field-freight__slip:has(.field-freight__input:focus-visible) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), 0 0 0 2px light-dark(#5f626a, #a7abb2); }
.field-freight__slip[data-full='0'] { animation: field-freight-full-a 0.3s; }
.field-freight__slip[data-full='1'] { animation: field-freight-full-b 0.3s; }
@keyframes field-freight-full-a { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
@keyframes field-freight-full-b { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
.field-freight__input { flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); font: 500 14px system-ui, sans-serif; outline: none; }
.field-freight__input::placeholder { color: light-dark(#6d7179, #7d8087); }
.field-freight__key { padding: 3px 6px; border-radius: 5px; background: light-dark(#ebedf0, #232529); color: light-dark(#5f626a, #a7abb2); font: 600 10px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
@media (prefers-reduced-motion: reduce) {
.field-freight * { animation: none !important; }
.field-freight__gone { display: none; }
}JavaScript
function initFieldFreight(root) {
var MAX = 3;
var yard = root.querySelector('.field-freight__yard');
var train = root.querySelector('.field-freight__train');
var loco = root.querySelector('.field-freight__loco');
var list = root.querySelector('.field-freight__train ul');
var slip = root.querySelector('.field-freight__slip');
var input = root.querySelector('.field-freight__input');
var tags = [].slice.call(list.querySelectorAll('.field-freight__tag')).map(function (t) {
return t.textContent;
});
var adds = 0;
var full = 0;
var gone = null;
function width(t) {
return Math.max(38, Math.min(64, 14 + t.length * 5.8));
}
function wheels(at) {
return at.map(function (l) {
return '<span class="field-freight__wheel" style="left:' + l + 'px"></span>';
}).join('');
}
function wagon(t, style) {
var w = document.createElement('span');
w.className = 'field-freight__wagon';
w.style.cssText = 'width:' + width(t) + 'px;' + (style || '');
w.innerHTML = '<span class="field-freight__tag"></span>' + wheels([5, width(t) - 14]);
w.firstChild.textContent = t;
return w;
}
// A new wagon rolls in from the right and couples on.
function car(t) {
var li = document.createElement('li');
li.style.cssText = 'display:flex;align-items:flex-end';
li.innerHTML = '<span class="field-freight__link" aria-hidden="true"></span>';
var w = wagon(t);
var off = document.createElement('button');
off.type = 'button';
off.className = 'field-freight__off';
off.setAttribute('aria-label', 'Remove ' + t);
off.textContent = '×';
w.appendChild(off);
li.appendChild(w);
return li;
}
function hint() {
input.placeholder = tags.length >= MAX ? 'Train is full' : 'Add a tag';
}
function add() {
var t = input.value.trim().slice(0, 9);
if (!t) return;
// A full train, or a tag it already carries: the slip shakes instead.
if (tags.length >= MAX || tags.indexOf(t) !== -1) return slip.setAttribute('data-full', String(full++ % 2));
tags.push(t);
list.appendChild(car(t));
input.value = '';
train.setAttribute('data-jolt', String(adds++ % 2));
// A fresh puff of smoke for every coupling.
var puff = loco.querySelector('.field-freight__puff');
var fresh = puff.cloneNode(false);
puff.replaceWith(fresh);
hint();
}
function remove(i) {
// The uncoupled wagon rolls away from where it stood.
var x = 4 + 60 + tags.slice(0, i).reduce(function (s, t) {
return s + 6 + width(t);
}, 0) + 6;
if (gone) gone.remove();
gone = document.createElement('span');
gone.className = 'field-freight__gone';
gone.style.left = x + 'px';
gone.setAttribute('aria-hidden', 'true');
gone.appendChild(wagon(tags[i], 'display:block;animation:none'));
gone.addEventListener('animationend', function (e) {
e.currentTarget.remove();
});
yard.appendChild(gone);
list.children[i].remove();
tags.splice(i, 1);
hint();
}
function onKey(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
add();
} else if (e.key === 'Backspace' && !input.value && tags.length) {
remove(tags.length - 1);
}
}
function onClick(e) {
var off = e.target.closest('.field-freight__off');
if (!off) return;
var li = off.closest('li');
remove([].indexOf.call(list.children, li));
}
input.addEventListener('keydown', onKey);
list.addEventListener('click', onClick);
return function () {
input.removeEventListener('keydown', onKey);
list.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.field-freight').forEach(initFieldFreight);Goal: Create a "Freight" input.
Visual behavior: A tag field as a goods train: press Enter and a wagon with your tag rolls in and couples on with a jolt and a puff of smoke; Backspace in the empty field uncouples the last one.
Technique:
- A real text input adds a tag on Enter or a comma and removes the last one on Backspace when empty; each wagon carries its own remove button.
- A new wagon rolls in from the right with one keyframe, and the whole train jolts back as it couples (two alternating keyframes on a data attribute) while the engine puffs smoke.
- A removed wagon is replaced by a copy at the same spot that rolls away off the end of the line, so the train closes up behind it.
- Three wagons is a full train: another tag, or a repeat, shakes the field instead.
Accessibility: A native input named "Add a tag" with a placeholder that says when the train is full. The tags are a list labelled "Tags", and each wagon's remove button is named "Remove <tag>" and shows on hover or keyboard focus. The engine and the rolling copies are aria-hidden. Under prefers-reduced-motion wagons appear and leave without rolling.
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 "Freight" input.
Visual behavior: A tag field as a goods train: press Enter and a wagon with your tag rolls in and couples on with a jolt and a puff of smoke; Backspace in the empty field uncouples the last one.
Technique:
- A real text input adds a tag on Enter or a comma and removes the last one on Backspace when empty; each wagon carries its own remove button.
- A new wagon rolls in from the right with one keyframe, and the whole train jolts back as it couples (two alternating keyframes on a data attribute) while the engine puffs smoke.
- A removed wagon is replaced by a copy at the same spot that rolls away off the end of the line, so the train closes up behind it.
- Three wagons is a full train: another tag, or a repeat, shakes the field instead.
Accessibility: A native input named "Add a tag" with a placeholder that says when the train is full. The tags are a list labelled "Tags", and each wagon's remove button is named "Remove <tag>" and shows on hover or keyboard focus. The engine and the rolling copies are aria-hidden. Under prefers-reduced-motion wagons appear and leave without rolling.
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 tag field as a goods train: press Enter and a wagon with your tag rolls in and couples on with a jolt and a puff of smoke; Backspace in the empty field uncouples the last one.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, click, keyframe
- Techniques
- keyframes, state machine, css variables
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native input named "Add a tag" with a placeholder that says when the train is full. The tags are a list labelled "Tags", and each wagon's remove button is named "Remove <tag>" and shows on hover or keyboard focus. The engine and the rolling copies are aria-hidden. Under prefers-reduced-motion wagons appear and leave without rolling.