@use 'sprucecss/scss/spruce' as *;
.tag-input {
@include layout-stack('xs');
&__box {
align-items: center;
background-color: color('background', 'form');
border: config('border-width', $form-control, false) solid color('border', 'form');
border-radius: config('border-radius', $form-control, false);
cursor: text;
display: flex;
flex-wrap: wrap;
gap: spacer('xxs');
padding: spacer('xxs');
@include transition($property: 'border-color, box-shadow');
&:focus-within {
@include short-ring('input');
}
}
&__list {
@include clear-list;
display: contents;
}
&__tag {
align-items: center;
background-color: color-alpha('primary', 0.12);
border-radius: radius('sm');
color: color('heading');
display: inline-flex;
font-size: config('font-size-sm', $typography);
gap: spacer('xxs');
margin: 0 !important;
padding: 0.2em 0.25em 0.2em 0.6em;
}
&__remove {
@include clear-btn;
border-radius: radius('sm');
font-size: 1.15em;
line-height: 1;
padding: 0 0.3em;
&:hover,
&:focus-visible {
background-color: color-alpha('primary', 0.2);
color: color('primary');
}
&:focus-visible {
outline: 2px solid color('focus-ring');
}
}
&__field {
background: transparent;
border: 0;
color: color('text', 'form');
flex: 1 0 8rem;
font: inherit;
padding: 0.25em 0.5em;
&:focus {
outline: none;
}
}
}
<div class="tag-input" data-tag-input>
<label class="form-label" for="tag-input-field">Skills</label>
<div class="tag-input__box">
<ul class="tag-input__list" role="list" aria-label="Selected skills">
<li class="tag-input__tag"><span>CSS</span><button class="tag-input__remove" type="button" aria-label="Remove CSS">×</button></li>
<li class="tag-input__tag"><span>Sass</span><button class="tag-input__remove" type="button" aria-label="Remove Sass">×</button></li>
</ul>
<input class="tag-input__field" id="tag-input-field" type="text" aria-describedby="tag-input-help" autocomplete="off">
</div>
<p class="form-description" id="tag-input-help">Press Enter or comma to add a skill, Backspace to remove the last one.</p>
<p class="sr-only" aria-live="polite" data-tag-status></p>
<input type="hidden" name="skills" value="CSS,Sass" data-tag-value>
</div>
(() => {
document.querySelectorAll('[data-tag-input]').forEach((component) => {
const list = component.querySelector('.tag-input__list');
const field = component.querySelector('.tag-input__field');
const status = component.querySelector('[data-tag-status]');
const hidden = component.querySelector('[data-tag-value]');
const values = () => [...list.querySelectorAll('.tag-input__tag span')].map((tag) => tag.textContent);
const sync = () => { hidden.value = values().join(','); };
const remove = (tag) => {
const name = tag.querySelector('span').textContent;
tag.remove();
sync();
status.textContent = `${name} removed.`;
field.focus();
};
const add = (value) => {
const name = value.trim();
if (!name || values().some((item) => item.toLowerCase() === name.toLowerCase())) {
return;
}
const tag = document.createElement('li');
const label = document.createElement('span');
const button = document.createElement('button');
tag.className = 'tag-input__tag';
label.textContent = name;
button.className = 'tag-input__remove';
button.type = 'button';
button.textContent = '×';
button.setAttribute('aria-label', `Remove ${name}`);
tag.append(label, button);
list.append(tag);
sync();
status.textContent = `${name} added.`;
};
component.querySelector('.tag-input__box').addEventListener('click', (event) => {
const removeButton = event.target.closest('.tag-input__remove');
if (removeButton) {
remove(removeButton.closest('.tag-input__tag'));
} else {
field.focus();
}
});
field.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ',') {
event.preventDefault();
add(field.value);
field.value = '';
} else if (event.key === 'Backspace' && !field.value) {
const last = list.querySelector('.tag-input__tag:last-child');
if (last) {
remove(last);
}
}
});
});
})();