Labels
Summary
A label is a tag chip for folksonomy the user
edits: topics on projects, tags on issues. Render it as an
<a> to make it navigate to the tagged
collection, and nest a .soma-label-remove button when
the user may take the tag off. It is deliberately quieter than a
badge: labels describe, they don't alarm.
When to use
| Component | Use it for |
|---|---|
| Label | User-managed tags: add, link, remove. The set is open-ended and edited by people. |
| Badge | Read-only status and counts the system assigns — healthy, failed, 3 pending. Never clickable; see Badges. |
| Filter chip | Toggleable filters above tables and lists, state on aria-pressed; see Filter chips. |
Examples
Removal wiring
Soma supplies the affordance (the nested × button with its danger-tinted hover) and leaves the semantics to you, because what removing a tag means (a DELETE call, a form-field update, nothing until Save) is application-specific. Three rules keep the pattern solid:
- Name the tag in the button's
aria-label: "Remove label frontend", never a bare "Remove". A row of anonymous × buttons is unusable in a screen-reader's controls list. - Delegate one listener over the labels container instead of binding each button — labels come and go.
- Mind keyboard focus: removing the label destroys the focused button. Move focus somewhere sensible (the next label's remove button, or the tag-input field) so a keyboard user isn't dropped at the document root.
HTML
The plain form: a span, purely descriptive:
<span class="soma-label">frontend</span>
Linkable: the same class on an anchor adds hover/focus styling; point it at the tagged collection:
<a class="soma-label" href="/tags/frontend">frontend</a>
Removable: nest the × button and name the target in its
aria-label:
<span class="soma-label">
frontend
<button class="soma-label-remove" aria-label="Remove label frontend">
<span class="soma-icon soma-icon-close"></span>
</button>
</span>
CSS classes
| Class | Effect |
|---|---|
.soma-label | The tag chip. On a <span> it is static; on an <a> it gains link hover/focus styling. |
.soma-label-remove | The × button nested inside a removable label, danger-tinted on hover. Always give it an aria-label that names the tag ("Remove label frontend"), not just "Remove". |
JavaScript
None shipped — the label is CSS-only. Removal is a one-line listener in your page, plus whatever your backend needs.
The minimal wiring: one delegated listener over the container:
container.addEventListener('click', (e) => {
const btn = e.target.closest('.soma-label-remove');
if (btn) btn.closest('.soma-label').remove();
});
With persistence — remove optimistically, restore on failure:
container.addEventListener('click', async (e) => {
const btn = e.target.closest('.soma-label-remove');
if (!btn) return;
const label = btn.closest('.soma-label');
const tag = label.textContent.trim();
label.remove(); // optimistic
try {
await fetch(`/api/issues/42/labels/${encodeURIComponent(tag)}`,
{ method: 'DELETE' });
} catch {
container.append(label); // restore on failure
Soma.toast({ body: `Could not remove "${tag}"`,
appearance: 'danger', close: 'manual' });
}
});
Adding a label dynamically: build the same markup contract,
including the named aria-label:
function addLabel(container, tag) {
const label = document.createElement('span');
label.className = 'soma-label';
label.append(tag);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'soma-label-remove';
btn.setAttribute('aria-label', `Remove label ${tag}`);
btn.innerHTML = '<span class="soma-icon soma-icon-close"></span>';
label.append(btn);
container.append(label);
}
The live example above, exactly as this page wires it:
// Removal demo: the component ships no JS — this is the consumer's
// one-liner.
document.getElementById('labels-removable').addEventListener('click', (e) => {
const btn = e.target.closest('.soma-label-remove');
if (btn) btn.closest('.soma-label').remove();
});