Skills Picker
A multiple select that shows the selection as removable tags, while the trigger shows a count. The tags are rebuilt on every valuechange, and removing one assigns a filtered value.
Code
The following example assumes that you have imported the component and set up the theme.
<div class="skills-picker">41 collapsed lines
<label class="field-label" for="skills-trigger">Skills</label>
<div class="chips"></div>
<sv-select class="skills-select" multiple> <sv-flyout> <button id="skills-trigger" type="button" class="trigger" slot="trigger"> <span data-select-no-value>Choose your skills</span> <span class="summary" data-select-has-value></span>
<svg class="chevron" aria-hidden="true" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg"> <path d="M480-360 280-560h400L480-360Z"></path> </svg> </button>
<button class="option" value="typescript"> <span class="mark"></span> TypeScript </button> <button class="option" value="css"> <span class="mark"></span> CSS architecture </button> <button class="option" value="accessibility"> <span class="mark"></span> Accessibility </button> <button class="option" value="testing"> <span class="mark"></span> Testing </button> <button class="option" value="performance"> <span class="mark"></span> Performance </button> <button class="option" value="build-tools"> <span class="mark"></span> Build tools </button> </sv-flyout> </sv-select></div>
<!-- Chip --><template id="skill-chip">12 collapsed lines
<button type="button" class="chip"> <!-- Remove --> <svg aria-hidden="true" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg"> <path d="M300-300l360-360M300-660l360 360" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-width="110" ></path> </svg> </button></template>
<script type="application/x-typescript">32 collapsed lines
const skillsSelect = document.querySelector<Select>(".skills-select")!; const chipList = document.querySelector(".chips")!; const chipTemplate = document.querySelector<HTMLTemplateElement>("#skill-chip")!; const summary = document.querySelector(".summary")!;
const getSelectedSkills = () => skillsSelect.value as string[];
const getSkillLabel = (value: string) => { return skillsSelect.querySelector(`.option[value="${value}"]`)!.textContent.trim(); };
const createChip = (value: string) => { const chip = chipTemplate.content.firstElementChild!.cloneNode(true) as HTMLButtonElement; const label = getSkillLabel(value);
chip.prepend(label); chip.ariaLabel = `Remove ${label}`; chip.addEventListener("click", () => { skillsSelect.value = getSelectedSkills().filter(selected => selected !== value); });
return chip; };
skillsSelect.addEventListener("valuechange", () => { const selectedSkills = getSelectedSkills();
summary.textContent = `${selectedSkills.length} selected`; chipList.replaceChildren(...selectedSkills.map(createChip)); });
skillsSelect.value = ["typescript", "accessibility"];</script>
<style>144 collapsed lines
.skills-picker { display: grid; gap: 0.5em; inline-size: min(100%, 24rem); margin-inline: auto; color: var(--sv-gray-2);
.field-label { font-weight: 600; }
.chips { display: flex; flex-wrap: wrap; gap: 0.4em;
&:empty { display: none; } }
.chip { display: flex; gap: 0.4em; align-items: center; padding: 0.25em 0.5em 0.25em 0.6em; font: inherit; font-size: 0.875em; color: var(--sv-gray-1); cursor: pointer; background-color: var(--sv-gray-5); border: none; border-radius: var(--sv-bdr-rad-sm);
svg { flex: none; inline-size: 0.75em; block-size: 0.75em; color: var(--sv-gray-3); }
@media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--sv-gray-4);
svg { color: var(--sv-gray-1); } } } }
.trigger { display: flex; gap: 0.5em; align-items: center; justify-content: space-between; }
.chevron { flex: none; inline-size: 1.25em; block-size: 1.25em; fill: currentcolor; rotate: -90deg; transition: rotate 200ms ease; }
.option { display: flex; gap: 0.7em; align-items: center; transition: background-color 160ms ease;
.mark { display: grid; flex: none; place-items: center; inline-size: 1.35em; block-size: 1.35em; border: solid var(--sv-bdr-sz-md) var(--sv-gray-4); border-radius: var(--sv-bdr-rad-sm); transition: background-color 160ms ease, border-color 160ms ease;
/* Check */ &::after { inline-size: 0.3em; block-size: 0.6em; content: ""; border: solid var(--sv-accent-content); border-width: 0 0.14em 0.14em 0; opacity: 0; rotate: 45deg; scale: 0.5; translate: 0 -0.08em; transition: opacity 160ms ease, scale 160ms cubic-bezier(0.25, 1, 0.5, 1); } }
&[aria-selected="true"] { color: var(--sv-gray-1); background-color: color-mix(in srgb, var(--sv-accent) 14%, transparent);
.mark { background-color: var(--sv-accent); border-color: var(--sv-accent);
&::after { opacity: 1; scale: 1; } } }
@media (hover: hover) and (pointer: fine) { &:not([aria-selected="true"]):hover { background-color: var(--sv-gray-5); } } }
sv-flyout:state(--opened) { .trigger { border-color: var(--sv-accent); }
.chevron { rotate: 0deg; } }
@media (prefers-reduced-motion: reduce) { .chevron, .option, .mark, .mark::after { transition: none; } } }</style>