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

ComponentUse it for
LabelUser-managed tags: add, link, remove. The set is open-ended and edited by people.
BadgeRead-only status and counts the system assigns — healthy, failed, 3 pending. Never clickable; see Badges.
Filter chipToggleable filters above tables and lists, state on aria-pressed; see Filter chips.

Examples

Plain

A span — static, descriptive:

frontend design-tokens cortex

Linked

The same class on an anchor — hover/focus styling:

Removable

Click × to take a tag off (live):

frontend needs-review cortex

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

ClassEffect
.soma-labelThe tag chip. On a <span> it is static; on an <a> it gains link hover/focus styling.
.soma-label-removeThe × 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();
});